Atributos Privados y Propiedades

Intermediate

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

Introducción

En este laboratorio, aprenderá cómo encapsular los elementos internos de un objeto utilizando atributos privados e implementar decoradores de propiedad (property decorators) para controlar el acceso a los atributos. Estas técnicas son esenciales para mantener la integridad de sus objetos y garantizar el manejo adecuado de los datos.

También comprenderá cómo restringir la creación de atributos utilizando __slots__. Modificaremos el archivo stock.py a lo largo de este laboratorio para aplicar estos conceptos.

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 81%. Ha recibido una tasa de reseñas positivas del 96% por parte de los estudiantes.

Implementación de Atributos Privados

En Python, utilizamos una convención de nomenclatura para indicar que un atributo está destinado al uso interno dentro de una clase. Prefijamos estos atributos con un guion bajo (_). Esto indica a otros desarrolladores que estos atributos no forman parte de la API pública y no deben accederse directamente desde fuera de la clase.

Veamos la clase Stock actual en el archivo stock.py. Tiene una variable de clase llamada types.

class Stock:
    ## Class variable for type conversions
    types = (str, int, float)

    ## Rest of the class...

La variable de clase types se utiliza internamente para convertir los datos de las filas. Para indicar que se trata de un detalle de implementación, la marcaremos como privada.

Instrucciones:

  1. Abra el archivo stock.py en el editor.

  2. Modifique la variable de clase types añadiendo un guion bajo al principio, cambiándola a _types.

    class Stock:
        ## Class variable for type conversions
        _types = (str, int, float)
    
        ## Rest of the class...
  3. Actualice el método from_row para utilizar la variable renombrada _types.

    @classmethod
    def from_row(cls, row):
        values = [func(val) for func, val in zip(cls._types, row)]
        return cls(*values)
  4. Guarde el archivo stock.py.

  5. Cree un script de Python llamado test_stock.py para probar sus cambios. Puede crear el archivo en el editor utilizando el siguiente comando:

    touch /home/labex/project/test_stock.py
  6. Añada el siguiente código al archivo test_stock.py. Este código crea instancias de la clase Stock e imprime información sobre ellas.

    from stock import Stock
    
    ## Create a stock instance
    s = Stock('GOOG', 100, 490.10)
    print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}")
    print(f"Cost: {s.cost()}")
    
    ## Create from row
    row = ['AAPL', '50', '142.5']
    apple = Stock.from_row(row)
    print(f"Name: {apple.name}, Shares: {apple.shares}, Price: {apple.price}")
    print(f"Cost: {apple.cost()}")
  7. Ejecute el script de prueba utilizando el siguiente comando en la terminal:

    python /home/labex/project/test_stock.py

    Debería ver una salida similar a:

    Name: GOOG, Shares: 100, Price: 490.1
    Cost: 49010.0
    Name: AAPL, Shares: 50, Price: 142.5
    Cost: 7125.0

Conversión de Métodos a Propiedades

Las propiedades (properties) en Python le permiten acceder a valores calculados como si fueran atributos. Esto elimina la necesidad de paréntesis al llamar a un método, lo que hace que su código sea más limpio y consistente.

Actualmente, nuestra clase Stock tiene un método cost() que calcula el costo total de las acciones.

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

Para obtener el valor del costo, tenemos que llamarlo con paréntesis:

s = Stock('GOOG', 100, 490.10)
print(s.cost())  ## Calls the method

Podemos mejorar esto convirtiendo el método cost() en una propiedad, lo que nos permite acceder al valor del costo sin paréntesis:

s = Stock('GOOG', 100, 490.10)
print(s.cost)  ## Accesses the property

Instrucciones:

  1. Abra el archivo stock.py en el editor.

  2. Reemplace el método cost() con una propiedad utilizando el decorador @property:

    @property
    def cost(self):
        return self.shares * self.price
  3. Guarde el archivo stock.py.

  4. Cree un nuevo archivo llamado test_property.py en el editor:

    touch /home/labex/project/test_property.py
  5. Añada el siguiente código al archivo test_property.py para crear una instancia de Stock y acceder a la propiedad cost:

    from stock import Stock
    
    ## Create a stock instance
    s = Stock('GOOG', 100, 490.10)
    
    ## Access cost as a property (no parentheses)
    print(f"Stock: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price}")
    print(f"Cost: {s.cost}")  ## Using the property
  6. Ejecute el script de prueba:

    python /home/labex/project/test_property.py

    Debería ver una salida similar a:

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

Implementación de la Validación de Propiedades

Las propiedades (properties) también le permiten controlar cómo se recuperan, establecen y eliminan los valores de los atributos. Esto es útil para añadir validación a sus atributos, asegurando que los valores cumplen criterios específicos.

En nuestra clase Stock, queremos asegurar que shares sea un entero no negativo y que price sea un flotante no negativo. Utilizaremos decoradores de propiedad junto con getters y setters para lograr esto.

Instrucciones:

  1. Abra el archivo stock.py en el editor.

  2. Añada atributos privados _shares y _price a la clase Stock y modifique el constructor para utilizarlos:

    def __init__(self, name, shares, price):
        self.name = name
        self._shares = shares  ## Using private attribute
        self._price = price    ## Using private attribute
  3. Defina propiedades para shares y price con validación:

    @property
    def shares(self):
        return self._shares
    
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError("Expected integer")
        if value < 0:
            raise ValueError("shares must be >= 0")
        self._shares = value
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        if not isinstance(value, float):
            raise TypeError("Expected float")
        if value < 0:
            raise ValueError("price must be >= 0")
        self._price = value
  4. Actualice el constructor para utilizar los setters de propiedad para la validación:

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares  ## Using property setter
        self.price = price    ## Using property setter
  5. Guarde el archivo stock.py.

  6. Cree un script de prueba llamado test_validation.py:

    touch /home/labex/project/test_validation.py
  7. Añada el siguiente código al archivo test_validation.py:

    from stock import Stock
    
    ## Create a valid stock instance
    s = Stock('GOOG', 100, 490.10)
    print(f"Initial: Name={s.name}, Shares={s.shares}, Price={s.price}, Cost={s.cost}")
    
    ## Test valid updates
    try:
        s.shares = 50  ## Valid update
        print(f"After setting shares=50: Shares={s.shares}, Cost={s.cost}")
    except Exception as e:
        print(f"Error setting shares=50: {e}")
    
    try:
        s.price = 123.45  ## Valid update
        print(f"After setting price=123.45: Price={s.price}, Cost={s.cost}")
    except Exception as e:
        print(f"Error setting price=123.45: {e}")
    
    ## Test invalid updates
    try:
        s.shares = "50"  ## Invalid type (string)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting shares='50': {e}")
    
    try:
        s.shares = -10  ## Invalid value (negative)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting shares=-10: {e}")
    
    try:
        s.price = "123.45"  ## Invalid type (string)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting price='123.45': {e}")
    
    try:
        s.price = -10.0  ## Invalid value (negative)
        print("This line should not execute")
    except Exception as e:
        print(f"Error setting price=-10.0: {e}")
  8. Ejecute el script de prueba:

    python /home/labex/project/test_validation.py

    Debería ver una salida que muestre actualizaciones válidas exitosas y mensajes de error apropiados para actualizaciones no válidas.

    Initial: Name=GOOG, Shares=100, Price=490.1, Cost=49010.0
    After setting shares=50: Shares=50, Cost=24505.0
    After setting price=123.45: Price=123.45, Cost=6172.5
    Error setting shares='50': Expected integer
    Error setting shares=-10: shares must be >= 0
    Error setting price='123.45': Expected float
    Error setting price=-10.0: price must be >= 0

Usando __slots__ para la Optimización de Memoria

El atributo __slots__ restringe los atributos que una clase puede tener. Evita añadir nuevos atributos a las instancias y reduce el uso de memoria.

En nuestra clase Stock, utilizaremos __slots__ para:

  1. Restringir la creación de atributos solo a los atributos que hemos definido.
  2. Mejorar la eficiencia de la memoria, especialmente al crear muchas instancias.

Instrucciones:

  1. Abra el archivo stock.py en el editor.

  2. Añada una variable de clase __slots__, listando todos los nombres de atributos privados utilizados por la clase:

    class Stock:
        ## Class variable for type conversions
        _types = (str, int, float)
    
        ## Define slots to restrict attribute creation
        __slots__ = ('name', '_shares', '_price')
    
        ## Rest of the class...
  3. Guarde el archivo.

  4. Cree un script de prueba llamado test_slots.py:

    touch /home/labex/project/test_slots.py
  5. Añada el siguiente código al archivo test_slots.py:

    from stock import Stock
    
    ## Create a stock instance
    s = Stock('GOOG', 100, 490.10)
    
    ## Access existing attributes
    print(f"Name: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price}")
    print(f"Cost: {s.cost}")
    
    ## Try to add a new attribute
    try:
        s.extra = "This will fail"
        print(f"Extra: {s.extra}")
    except AttributeError as e:
        print(f"Error: {e}")
  6. Ejecute el script de prueba:

    python /home/labex/project/test_slots.py

    Debería ver una salida que muestre que puede acceder a los atributos definidos, pero al intentar añadir un nuevo atributo se genera un AttributeError.

    Name: GOOG
    Shares: 100
    Price: 490.1
    Cost: 49010.0
    Error: 'Stock' object has no attribute 'extra'

Conciliando la Validación de Tipos con Variables de Clase

Actualmente, nuestra clase Stock utiliza tanto la variable de clase _types como los setters de propiedad para el manejo de tipos. Para mejorar la consistencia y la mantenibilidad, reconciliaremos estos mecanismos para que utilicen la misma información de tipo.

Instrucciones:

  1. Abra el archivo stock.py en el editor.

  2. Modifique los setters de propiedad para utilizar los tipos definidos en la variable de clase _types:

    @property
    def shares(self):
        return self._shares
    
    @shares.setter
    def shares(self, value):
        if not isinstance(value, self._types[1]):
            raise TypeError(f"Expected {self._types[1].__name__}")
        if value < 0:
            raise ValueError("shares must be >= 0")
        self._shares = value
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        if not isinstance(value, self._types[2]):
            raise TypeError(f"Expected {self._types[2].__name__}")
        if value < 0:
            raise ValueError("price must be >= 0")
        self._price = value
  3. Guarde el archivo stock.py.

  4. Cree un script de prueba llamado test_subclass.py:

    touch /home/labex/project/test_subclass.py
  5. Añada el siguiente código al archivo test_subclass.py:

    from stock import Stock
    from decimal import Decimal
    
    ## Create a subclass with different types
    class DStock(Stock):
        _types = (str, int, Decimal)
    
    ## Test the base class
    s = Stock('GOOG', 100, 490.10)
    print(f"Stock: {s.name}, Shares: {s.shares}, Price: {s.price}, Cost: {s.cost}")
    
    ## Test valid update with float
    try:
        s.price = 500.25
        print(f"Updated Stock price: {s.price}, Cost: {s.cost}")
    except Exception as e:
        print(f"Error updating Stock price: {e}")
    
    ## Test the subclass with Decimal
    ds = DStock('AAPL', 50, Decimal('142.50'))
    print(f"DStock: {ds.name}, Shares: {ds.shares}, Price: {ds.price}, Cost: {ds.cost}")
    
    ## Test invalid update with float (should require Decimal)
    try:
        ds.price = 150.75
        print(f"Updated DStock price: {ds.price}")
    except Exception as e:
        print(f"Error updating DStock price: {e}")
    
    ## Test valid update with Decimal
    try:
        ds.price = Decimal('155.25')
        print(f"Updated DStock price: {ds.price}, Cost: {ds.cost}")
    except Exception as e:
        print(f"Error updating DStock price: {e}")
  6. Ejecute el script de prueba:

    python /home/labex/project/test_subclass.py

    Debería ver que la clase base Stock acepta valores float para el precio, mientras que la subclase DStock requiere valores Decimal.

    Stock: GOOG, Shares: 100, Price: 490.1, Cost: 49010.0
    Updated Stock price: 500.25, Cost: 50025.0
    DStock: AAPL, Shares: 50, Price: 142.50, Cost: 7125.00
    Error updating DStock price: Expected Decimal
    Updated DStock price: 155.25, Cost: 7762.50

Resumen

En este laboratorio, ha aprendido cómo utilizar atributos privados, convertir métodos en propiedades, implementar la validación de propiedades, usar __slots__ para la optimización de memoria y conciliar la validación de tipos con variables de clase. Estas técnicas mejoran la robustez, la eficiencia y la mantenibilidad de sus clases al aplicar el encapsulamiento y proporcionar interfaces claras.