Introduction
In this lab, you'll learn how to encapsulate object internals using private attributes and implement property decorators to control attribute access. These techniques are essential for maintaining the integrity of your objects and ensuring proper data handling.
You'll also understand how to restrict attribute creation using __slots__. We'll modify the stock.py file throughout this lab to apply these concepts.
Implementing Private Attributes
In Python, we use a naming convention to indicate that an attribute is intended for internal use within a class. We prefix these attributes with an underscore (_). This signals to other developers that these attributes are not part of the public API and should not be accessed directly from outside the class.
Let's look at the current Stock class in the stock.py file. It has a class variable named types.
class Stock:
## Class variable for type conversions
types = (str, int, float)
## Rest of the class...
The types class variable is used internally to convert row data. To indicate that this is an implementation detail, we'll mark it as private.
Instructions:
Open the
stock.pyfile in the editor.Modify the
typesclass variable by adding a leading underscore, changing it to_types.class Stock: ## Class variable for type conversions _types = (str, int, float) ## Rest of the class...Update the
from_rowmethod to use the renamed variable_types.@classmethod def from_row(cls, row): values = [func(val) for func, val in zip(cls._types, row)] return cls(*values)Save the
stock.pyfile.Create a Python script named
test_stock.pyto test your changes. You can create the file in the editor using the following command:touch /home/labex/project/test_stock.pyAdd the following code to the
test_stock.pyfile. This code creates instances of theStockclass and prints information about them.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()}")Run the test script using the following command in the terminal:
python /home/labex/project/test_stock.pyYou should see output similar to:
Name: GOOG, Shares: 100, Price: 490.1 Cost: 49010.0 Name: AAPL, Shares: 50, Price: 142.5 Cost: 7125.0
Converting Methods to Properties
Properties in Python allow you to access computed values like attributes. This eliminates the need for parentheses when calling a method, making your code cleaner and more consistent.
Currently, our Stock class has a cost() method that calculates the total cost of the shares.
def cost(self):
return self.shares * self.price
To get the cost value, we have to call it with parentheses:
s = Stock('GOOG', 100, 490.10)
print(s.cost()) ## Calls the method
We can improve this by converting the cost() method to a property, allowing us to access the cost value without parentheses:
s = Stock('GOOG', 100, 490.10)
print(s.cost) ## Accesses the property
Instructions:
Open the
stock.pyfile in the editor.Replace the
cost()method with a property using the@propertydecorator:@property def cost(self): return self.shares * self.priceSave the
stock.pyfile.Create a new file named
test_property.pyin the editor:touch /home/labex/project/test_property.pyAdd the following code to the
test_property.pyfile to create aStockinstance and access thecostproperty: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 propertyRun the test script:
python /home/labex/project/test_property.pyYou should see output similar to:
Stock: GOOG Shares: 100 Price: 490.1 Cost: 49010.0
Implementing Property Validation
Properties also allow you to control how attribute values are retrieved, set, and deleted. This is useful for adding validation to your attributes, ensuring that the values meet specific criteria.
In our Stock class, we want to ensure that shares is a non-negative integer and price is a non-negative float. We'll use property decorators along with getters and setters to achieve this.
Instructions:
Open the
stock.pyfile in the editor.Add private attributes
_sharesand_priceto theStockclass and modify the constructor to use them:def __init__(self, name, shares, price): self.name = name self._shares = shares ## Using private attribute self._price = price ## Using private attributeDefine properties for
sharesandpricewith validation:@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 = valueUpdate the constructor to use the property setters for validation:
def __init__(self, name, shares, price): self.name = name self.shares = shares ## Using property setter self.price = price ## Using property setterSave the
stock.pyfile.Create a test script named
test_validation.py:touch /home/labex/project/test_validation.pyAdd the following code to the
test_validation.pyfile: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}")Run the test script:
python /home/labex/project/test_validation.pyYou should see output showing successful valid updates and appropriate error messages for invalid updates.
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
Using __slots__ for Memory Optimization
The __slots__ attribute restricts the attributes a class can have. It prevents adding new attributes to instances and reduces memory usage.
In our Stock class, we'll use __slots__ to:
- Restrict attribute creation to only the attributes we've defined.
- Improve memory efficiency, especially when creating many instances.
Instructions:
Open the
stock.pyfile in the editor.Add a
__slots__class variable, listing all the private attribute names used by the class: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...Save the file.
Create a test script named
test_slots.py:touch /home/labex/project/test_slots.pyAdd the following code to the
test_slots.pyfile: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}")Run the test script:
python /home/labex/project/test_slots.pyYou should see output showing that you can access the defined attributes, but attempting to add a new attribute raises an
AttributeError.Name: GOOG Shares: 100 Price: 490.1 Cost: 49010.0 Error: 'Stock' object has no attribute 'extra'
Reconciling Type Validation with Class Variables
Currently, our Stock class uses both the _types class variable and property setters for type handling. To improve consistency and maintainability, we'll reconcile these mechanisms so that they use the same type information.
Instructions:
Open the
stock.pyfile in the editor.Modify the property setters to use the types defined in the
_typesclass variable:@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 = valueSave the
stock.pyfile.Create a test script named
test_subclass.py:touch /home/labex/project/test_subclass.pyAdd the following code to the
test_subclass.pyfile: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}")Run the test script:
python /home/labex/project/test_subclass.pyYou should see that the base
Stockclass accepts float values for the price, while theDStocksubclass requiresDecimalvalues.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
Summary
In this lab, you've learned how to use private attributes, convert methods into properties, implement property validation, use __slots__ for memory optimization, and reconcile type validation with class variables. These techniques enhance the robustness, efficiency, and maintainability of your classes by enforcing encapsulation and providing clear interfaces.