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.
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:
Abra el archivo
stock.pyen el editor.Modifique la variable de clase
typesañ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...Actualice el método
from_rowpara 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)Guarde el archivo
stock.py.Cree un script de Python llamado
test_stock.pypara probar sus cambios. Puede crear el archivo en el editor utilizando el siguiente comando:touch /home/labex/project/test_stock.pyAñada el siguiente código al archivo
test_stock.py. Este código crea instancias de la claseStocke 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()}")Ejecute el script de prueba utilizando el siguiente comando en la terminal:
python /home/labex/project/test_stock.pyDeberí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:
Abra el archivo
stock.pyen el editor.Reemplace el método
cost()con una propiedad utilizando el decorador@property:@property def cost(self): return self.shares * self.priceGuarde el archivo
stock.py.Cree un nuevo archivo llamado
test_property.pyen el editor:touch /home/labex/project/test_property.pyAñada el siguiente código al archivo
test_property.pypara crear una instancia deStocky acceder a la propiedadcost: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 propertyEjecute el script de prueba:
python /home/labex/project/test_property.pyDeberí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:
Abra el archivo
stock.pyen el editor.Añada atributos privados
_sharesy_pricea la claseStocky 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 attributeDefina propiedades para
sharesypricecon 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 = valueActualice 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 setterGuarde el archivo
stock.py.Cree un script de prueba llamado
test_validation.py:touch /home/labex/project/test_validation.pyAñ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}")Ejecute el script de prueba:
python /home/labex/project/test_validation.pyDeberí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:
- Restringir la creación de atributos solo a los atributos que hemos definido.
- Mejorar la eficiencia de la memoria, especialmente al crear muchas instancias.
Instrucciones:
Abra el archivo
stock.pyen el editor.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...Guarde el archivo.
Cree un script de prueba llamado
test_slots.py:touch /home/labex/project/test_slots.pyAñ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}")Ejecute el script de prueba:
python /home/labex/project/test_slots.pyDeberí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:
Abra el archivo
stock.pyen el editor.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 = valueGuarde el archivo
stock.py.Cree un script de prueba llamado
test_subclass.py:touch /home/labex/project/test_subclass.pyAñ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}")Ejecute el script de prueba:
python /home/labex/project/test_subclass.pyDebería ver que la clase base
Stockacepta valores float para el precio, mientras que la subclaseDStockrequiere valoresDecimal.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.