Aprende sobre Decoradores de Clase

Beginner

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

Introducción

En este laboratorio, aprenderá sobre decoradores de clases en Python y revisará y extenderá los descriptores de Python. Al combinar estos conceptos, puede crear estructuras de código potentes y limpias.

En este laboratorio, se basará en conceptos de descriptores anteriores y los extenderá utilizando decoradores de clases. Esta combinación le permite crear código más limpio y mantenible con capacidades de validación mejoradas. Los archivos a modificar son validate.py y structure.py.

Implementación de Verificación de Tipos con Descriptores

En este paso, crearemos una clase Stock que utiliza descriptores para la verificación de tipos. Pero primero, entendamos qué son los descriptores. Los descriptores son una característica realmente potente en Python. Le otorgan control sobre cómo se accede a los atributos en las clases.

Los descriptores son objetos que definen cómo se accede a los atributos en otros objetos. Lo hacen implementando métodos especiales como __get__, __set__ y __delete__. Estos métodos permiten a los descriptores gestionar cómo se recuperan, establecen y eliminan los atributos. Los descriptores son muy útiles para implementar validación, verificación de tipos y propiedades calculadas. Por ejemplo, puede usar un descriptor para asegurarse de que un atributo sea siempre un número positivo o una cadena de un formato determinado.

El archivo validate.py ya tiene clases validadoras (String, PositiveInteger, PositiveFloat). Podemos usar estas clases para validar los atributos de nuestra clase Stock.

Ahora, creemos nuestra clase Stock con descriptores.

  1. Primero, abra el archivo stock.py en su editor.

  2. Una vez abierto el archivo, reemplace el contenido del marcador de posición con el siguiente código:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    _fields = ('name', 'shares', 'price')
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

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

    def sell(self, nshares):
        self.shares -= nshares

## Create an __init__ method based on _fields
Stock.create_init()

Analicemos lo que hace este código. La tupla _fields define los atributos de la clase Stock. Estos son los nombres de los atributos que tendrán nuestros objetos Stock.

Los atributos name, shares y price se definen como objetos descriptor. El descriptor String() asegura que el atributo name sea una cadena. El descriptor PositiveInteger() se asegura de que el atributo shares sea un entero positivo. Y el descriptor PositiveFloat() garantiza que el atributo price sea un número de punto flotante positivo.

La propiedad cost es una propiedad calculada. Calcula el costo total del stock basándose en el número de acciones y el precio por acción.

El método sell se utiliza para reducir el número de acciones. Cuando llama a este método con un número de acciones a vender, resta ese número del atributo shares.

La línea Stock.create_init() crea dinámicamente un método __init__ para nuestra clase. Este método nos permite crear objetos Stock pasando los valores para los atributos name, shares y price.

  1. Después de agregar el código, guarde el archivo. Esto asegurará que sus cambios se guarden y se puedan usar cuando ejecute las pruebas.

  2. Ahora, ejecutemos las pruebas para verificar su implementación. Primero, cambie el directorio al directorio ~/project ejecutando el siguiente comando:

cd ~/project

Luego, ejecute las pruebas usando el siguiente comando:

python3 teststock.py

Si su implementación es correcta, debería ver una salida similar a esta:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Esta salida significa que todas las pruebas están pasando. ¡Los descriptores están validando correctamente los tipos de cada atributo!

Intentemos crear un objeto Stock en el intérprete de Python. Primero, asegúrese de estar en el directorio ~/project. Luego, ejecute el siguiente comando:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Debería ver la siguiente salida:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

¡Ha implementado con éxito descriptores para la verificación de tipos! Ahora, mejoremos este código aún más.

Creación de un Decorador de Clases para Validación

En el paso anterior, nuestra implementación funcionó, pero había una redundancia. Teníamos que especificar tanto la tupla _fields como los atributos de descriptor. Esto no es muy eficiente y podemos mejorarlo. En Python, los decoradores de clases son una herramienta potente que puede ayudarnos a simplificar este proceso. Un decorador de clases es una función que toma una clase como argumento, la modifica de alguna manera y luego devuelve la clase modificada. Al usar un decorador de clases, podemos extraer automáticamente la información de los campos de los descriptores, lo que hará que nuestro código sea más limpio y mantenible.

Creemos un decorador de clases para simplificar nuestro código. Aquí están los pasos que debe seguir:

  1. Primero, abra el archivo structure.py en su editor.

  2. A continuación, agregue el siguiente código en la parte superior del archivo structure.py, justo después de cualquier declaración de importación. Este código define nuestro decorador de clases:

from validate import Validator

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields list automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

Analicemos lo que hace este decorador:

  • Primero crea una lista vacía llamada validators. Luego, itera sobre todos los atributos de la clase usando vars(cls).items(). Si un atributo es una instancia de la clase Validator, agrega ese atributo a la lista validators.
  • Después de eso, establece el atributo _fields de la clase. Crea una lista de nombres a partir de los validadores en la lista validators y la asigna a cls._fields.
  • Finalmente, llama al método create_init() de la clase para generar el método __init__, y luego devuelve la clase modificada.
  1. Una vez que haya agregado el código, guarde el archivo structure.py. Guardar el archivo asegura que sus cambios se conserven.

  2. Ahora, necesitamos modificar nuestro archivo stock.py para usar este nuevo decorador. Abra el archivo stock.py en su editor.

  3. Actualice el archivo stock.py para usar el decorador validate_attributes. Reemplace el código existente con el siguiente:

## stock.py

from structure import Structure, validate_attributes
from validate import String, PositiveInteger, PositiveFloat

@validate_attributes
class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

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

    def sell(self, nshares):
        self.shares -= nshares

Observe los cambios que hemos realizado:

  • Agregamos el decorador @validate_attributes justo encima de la definición de la clase Stock. Esto le indica a Python que aplique el decorador validate_attributes a la clase Stock.
  • Eliminamos la declaración explícita de _fields porque el decorador se encargará de ello automáticamente.
  • También eliminamos la llamada a Stock.create_init() porque el decorador se encarga de crear el método __init__.

Como resultado, la clase ahora es más simple y limpia. El decorador se encarga de todos los detalles que solíamos manejar manualmente.

  1. Después de realizar estos cambios, debemos verificar que todo siga funcionando como se esperaba. Ejecute las pruebas nuevamente usando los siguientes comandos:
cd ~/project
python3 teststock.py

Si todo funciona correctamente, debería ver la siguiente salida:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Esta salida indica que todas las pruebas han pasado con éxito.

Probemos también nuestra clase Stock de forma interactiva. Ejecute el siguiente comando en la terminal:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Debería ver la siguiente salida:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

¡Genial! Ha implementado con éxito un decorador de clases que simplifica nuestro código al manejar automáticamente las declaraciones de campos y la inicialización. Esto hace que nuestro código sea más eficiente y fácil de mantener.

Aplicación de Decoradores mediante Herencia

En el Paso 2, creamos un decorador de clases que simplifica nuestro código. Un decorador de clases es un tipo especial de función que toma una clase como argumento y devuelve una clase modificada. Es una herramienta útil en Python para agregar funcionalidad a las clases sin modificar su código original. Sin embargo, todavía necesitamos aplicar explícitamente el decorador @validate_attributes a cada clase. Esto significa que cada vez que creamos una nueva clase que necesita validación, debemos recordar agregar este decorador, lo que puede ser un poco engorroso.

Podemos mejorar esto aún más aplicando el decorador automáticamente a través de la herencia. La herencia es un concepto fundamental en la programación orientada a objetos donde una subclase puede heredar atributos y métodos de una clase padre. El método __init_subclass__ de Python se introdujo en Python 3.6 para permitir que las clases padre personalicen la inicialización de las subclases. Esto significa que cuando se crea una subclase, la clase padre puede realizar algunas acciones sobre ella. Podemos usar esta característica para aplicar automáticamente nuestro decorador a cualquier clase que herede de Structure.

Implementemos esto:

  1. Abra el archivo structure.py en su editor. Este archivo contiene la definición de la clase Structure, y vamos a modificarla para usar el método __init_subclass__.

  2. Agregue el método __init_subclass__ a la clase Structure:

class Structure:
    _fields = ()
    _types = ()

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f'{type(self).__name__}({values})'

    @classmethod
    def create_init(cls):
        '''
        Create an __init__ method from _fields
        '''
        body = 'def __init__(self, %s):\n' % ', '.join(cls._fields)
        for name in cls._fields:
            body += f'    self.{name} = {name}\n'

        ## Execute the function creation code
        namespace = {}
        exec(body, namespace)
        setattr(cls, '__init__', namespace['__init__'])

    @classmethod
    def __init_subclass__(cls):
        validate_attributes(cls)

El método __init_subclass__ es un método de clase, lo que significa que se puede llamar en la clase misma en lugar de en una instancia de la clase. Cuando se crea una subclase de Structure, este método se llamará automáticamente. Dentro de este método, llamamos al decorador validate_attributes en la subclase cls. De esta manera, cada subclase de Structure tendrá automáticamente el comportamiento de validación.

  1. Guarde el archivo.

Después de realizar cambios en el archivo structure.py, debemos guardarlo para que se apliquen los cambios.

  1. Ahora, actualicemos nuestro archivo stock.py para aprovechar esta nueva característica. Abra el archivo stock.py en su editor para modificarlo. Este archivo contiene la definición de la clase Stock, y vamos a hacer que herede de la clase Structure para usar la aplicación automática del decorador.

  2. Modifique el archivo stock.py para eliminar el decorador explícito:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

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

    def sell(self, nshares):
        self.shares -= nshares

Tenga en cuenta que:

  • Eliminamos la importación de validate_attributes porque ya no necesitamos importarla explícitamente, ya que el decorador se aplica automáticamente a través de la herencia.
  • Eliminamos el decorador @validate_attributes porque el método __init_subclass__ en la clase Structure se encargará de aplicarlo.
  • El código ahora se basa únicamente en la herencia de Structure para obtener el comportamiento de validación.
  1. Vuelva a ejecutar las pruebas para verificar que todo sigue funcionando:
cd ~/project
python3 teststock.py

Ejecutar las pruebas es importante para asegurarnos de que nuestros cambios no hayan roto nada. Si todas las pruebas pasan, significa que la aplicación automática del decorador a través de la herencia está funcionando correctamente.

Debería ver que todas las pruebas pasan:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Probemos nuestra clase Stock nuevamente para asegurarnos de que funciona como se espera:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Este comando crea una instancia de la clase Stock e imprime su representación y el costo. Si la salida es la esperada, significa que la clase Stock está funcionando correctamente con la aplicación automática del decorador.

Salida:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

¡Esta implementación es aún más limpia! Al usar __init_subclass__, hemos eliminado la necesidad de aplicar decoradores explícitamente. Cualquier clase que herede de Structure obtiene automáticamente el comportamiento de validación.

Adición de Funcionalidad de Conversión de Filas

En programación, a menudo es útil crear instancias de una clase a partir de filas de datos, especialmente cuando se trabaja con datos de fuentes como archivos CSV. En esta sección, agregaremos la capacidad de crear instancias de la clase Structure a partir de filas de datos. Haremos esto implementando un método de clase from_row en la clase Structure.

  1. Primero, abra el archivo structure.py en su editor. Aquí es donde realizaremos los cambios en nuestro código.

  2. A continuación, modificaremos la función validate_attributes. Esta función es un decorador de clases que extrae instancias de Validator y construye las listas _fields y _types automáticamente. La actualizaremos para que también recopile información de tipos.

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields and _types lists automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

En esta función actualizada, estamos recopilando el atributo expected_type de cada validador y almacenándolo en la variable de clase _types. Esto será útil más adelante cuando convirtamos datos de filas a los tipos correctos.

  1. Ahora, agregaremos el método de clase from_row a la clase Structure. Este método nos permitirá crear una instancia de la clase a partir de una fila de datos, que podría ser una lista o una tupla.
@classmethod
def from_row(cls, row):
    """
    Create an instance from a data row (list or tuple)
    """
    rowdata = [func(val) for func, val in zip(cls._types, row)]
    return cls(*rowdata)

Así es como funciona este método:

  • Toma una fila de datos, que puede estar en forma de lista o tupla.
  • Convierte cada valor de la fila al tipo esperado utilizando la función correspondiente de la lista _types.
  • Luego crea y devuelve una nueva instancia de la clase utilizando los valores convertidos.
  1. Después de realizar estos cambios, guarde el archivo structure.py. Esto asegura que los cambios en su código se conserven.

  2. Probemos nuestro método from_row para asegurarnos de que funciona como se espera. Crearemos una prueba simple usando la clase Stock. Ejecute el siguiente comando en su terminal:

cd ~/project
python3 -c "from stock import Stock; s = Stock.from_row(['GOOG', '100', '490.1']); print(s); print(f'Cost: {s.cost}')"

Debería ver una salida similar a esta:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Observe que los valores de cadena '100' y '490.1' se convirtieron automáticamente a los tipos correctos (entero y flotante). Esto demuestra que nuestro método from_row está funcionando correctamente.

  1. Finalmente, intentemos leer datos de un archivo CSV usando nuestro módulo reader.py. Ejecute el siguiente comando en su terminal:
cd ~/project
python3 -c "from stock import Stock; import reader; portfolio = reader.read_csv_as_instances('portfolio.csv', Stock); print(portfolio); print(f'Total value: {sum(s.cost for s in portfolio)}')"

Debería ver una salida que muestre las acciones del archivo CSV:

[Stock('GOOG', 100, 490.1), Stock('AAPL', 50, 545.75), Stock('MSFT', 200, 30.47)]
Total value: 82391.5

El método from_row nos permite convertir fácilmente datos CSV en instancias de la clase Stock. Cuando se combina con la función read_csv_as_instances, tenemos una forma potente de cargar y trabajar con datos estructurados.

Adición de Validación de Argumentos de Métodos

En Python, validar datos es una parte importante para escribir código robusto. En esta sección, llevaremos nuestra validación un paso más allá validando automáticamente los argumentos de los métodos. El archivo validate.py ya incluye un decorador @validated. Un decorador en Python es una función especial que puede modificar otra función. El decorador @validated aquí puede verificar los argumentos de la función contra sus anotaciones. Las anotaciones en Python son una forma de agregar metadatos a los parámetros de la función y a los valores de retorno.

Modifiquemos nuestro código para aplicar este decorador a métodos con anotaciones:

  1. Primero, necesitamos entender cómo funciona el decorador validated. Abra el archivo validate.py en su editor para revisarlo.

El decorador validated utiliza anotaciones de función para validar argumentos. Antes de permitir que la función se ejecute, crea una instancia de la clase validadora para cada parámetro anotado y llama al método validate para verificar el argumento. Por ejemplo, si un argumento está anotado con PositiveInteger, el decorador creará una instancia de PositiveInteger y validará que el valor pasado sea efectivamente un entero positivo. Si la validación falla, recopila todos los errores y lanza un TypeError con mensajes de error detallados.

  1. Ahora, modificaremos la función validate_attributes en structure.py para envolver los métodos anotados con el decorador validated. Esto significa que cualquier método con anotaciones en la clase tendrá sus argumentos validados automáticamente. Abra el archivo structure.py en su editor.

  2. Actualice la función validate_attributes:

def validate_attributes(cls):
    """
    Class decorator that:
    1. Extracts Validator instances and builds _fields and _types lists
    2. Applies @validated decorator to methods with annotations
    """
    ## Import the validated decorator
    from validate import validated

    ## Process validator descriptors
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Apply @validated decorator to methods with annotations
    for name, val in vars(cls).items():
        if callable(val) and hasattr(val, '__annotations__'):
            setattr(cls, name, validated(val))

    ## Create initialization method
    cls.create_init()

    return cls

Esta función actualizada ahora hace lo siguiente:

  1. Procesa los descriptores de validación como antes. Los descriptores de validación se utilizan para definir reglas de validación para los atributos de clase.

  2. Encuentra todos los métodos con anotaciones en la clase. Las anotaciones se agregan a los parámetros del método para especificar el tipo esperado del argumento.

  3. Aplica el decorador @validated a esos métodos. Esto asegura que los argumentos pasados a estos métodos se validen de acuerdo con sus anotaciones.

  4. Guarde el archivo después de realizar estos cambios. Guardar el archivo es importante porque asegura que nuestras modificaciones se almacenen y se puedan usar más adelante.

  5. Ahora, actualicemos el método sell en la clase Stock para incluir una anotación. Las anotaciones ayudan a especificar el tipo esperado del argumento, que será utilizado por el decorador @validated para la validación. Abra el archivo stock.py en su editor.

  6. Modifique el método sell para incluir una anotación de tipo:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

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

    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

El cambio importante es agregar : PositiveInteger al parámetro nshares. Esto le dice a Python (y a nuestro decorador @validated) que valide este argumento usando el validador PositiveInteger. Por lo tanto, cuando llamemos al método sell, el argumento nshares debe ser un entero positivo.

  1. Vuelva a ejecutar las pruebas para verificar que todo sigue funcionando. Ejecutar pruebas es una buena manera de asegurarse de que nuestros cambios no hayan roto ninguna funcionalidad existente.
cd ~/project
python3 teststock.py

Debería ver que todas las pruebas pasan:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK
  1. Probemos nuestra nueva validación de argumentos. Intentaremos llamar al método sell con argumentos válidos e inválidos para ver si la validación funciona como se espera.
cd ~/project
python3 -c "
from stock import Stock
s = Stock('GOOG', 100, 490.1)
s.sell(25)
print(s)
try:
    s.sell(-25)
except Exception as e:
    print(f'Error: {e}')
"

Debería ver una salida similar a:

Stock('GOOG', 75, 490.1)
Error: Bad Arguments
  nshares: nshares must be >= 0

¡Esto demuestra que nuestra validación de argumentos de métodos está funcionando! La primera llamada a sell(25) tiene éxito porque 25 es un entero positivo. Pero la segunda llamada a sell(-25) falla porque -25 no es un entero positivo.

Ahora ha implementado un sistema completo para:

  1. Validar atributos de clase usando descriptores. Los descriptores se utilizan para definir reglas de validación para los atributos de clase.
  2. Recopilar automáticamente información de campos usando decoradores de clase. Los decoradores de clase pueden modificar el comportamiento de una clase, como la recopilación de información de campos.
  3. Convertir datos de filas en instancias. Esto es útil cuando se trabaja con datos de fuentes externas.
  4. Validar argumentos de métodos usando anotaciones. Las anotaciones ayudan a especificar el tipo esperado del argumento para la validación.

Esto demuestra el poder de combinar descriptores y decoradores en Python para crear clases expresivas y auto-validadoras.

Resumen

En este laboratorio, ha aprendido a combinar potentes características de Python para crear código limpio y auto-validador. Ha dominado conceptos clave como el uso de descriptores para la validación de atributos, la creación de decoradores de clase para la automatización de la generación de código y la aplicación automática de decoradores a través de la herencia.

Estas técnicas son herramientas potentes para crear código Python robusto y mantenible. Le permiten expresar claramente los requisitos de validación y aplicarlos en toda su base de código. Ahora puede aplicar estos patrones en sus propios proyectos de Python para mejorar la calidad del código y reducir el código repetitivo.