Property Decorators Advanced
Part of the Object Oriented Programming section of Coddy's Python journey — lesson 31 of 64.
Advanced property decorators provide more sophisticated control over attribute access, including computed properties, deleters, and full property management.
Here is an example of computed properties that derive values from other attributes:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
@property
def perimeter(self):
return 2 * (self.width + self.height)Use computed properties like regular attributes:
rect = Rectangle(5, 3)
print(rect.area) # 15 - calculated automatically
print(rect.perimeter) # 16 - calculated automaticallyCreate a property with getter, setter, and deleter:
class Temperature:
def __init__(self):
self._temp = 0
@property
def temperature(self):
return self._temp
@temperature.setter
def temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._temp = value
@temperature.deleter
def temperature(self):
print("Resetting temperature to 0")
self._temp = 0Use the full property functionality:
temp = Temperature()Use the setter with validation:
temp.temperature = 25
print(temp.temperature) # 25
# temp.temperature = -300 # Would raise ValueErrorUse the deleter:
del temp.temperature
print(temp.temperature) # 0Create a more complex example with a game score:
class Player:
def __init__(self, name):
self.name = name
self._score = 0
self._level = 1
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if value >= 0:
self._score = value
self._level = (value // 1000) + 1
else:
raise ValueError("Score cannot be negative")
@score.deleter
def score(self):
print(f"Resetting {self.name}'s progress")
self._score = 0
self._level = 1
@property
def level(self):
return self._level
player = Player("Alice")
player.score = 2500
print(f"Score: {player.score}, Level: {player.level}") # Score: 2500, Level: 3
del player.score
print(f"Score: {player.score}, Level: {player.level}") # Score: 0, Level: 1Output:
15
16
25
Resetting temperature to 0
0
Score: 2500, Level: 3
Resetting Alice's progress
Score: 0, Level: 1Key Point: Advanced property decorators allow computed properties (calculated from other data), property deletion with @property.deleter, and full control over getting, setting, and deleting attributes. This creates intuitive interfaces while maintaining strong data validation and encapsulation.
Challenge
MediumIn this challenge, you'll implement a Rectangle class with proper encapsulation and property validation.
rectangle.py- This is the file you need to edit, containing TODO comments to guide your implementationdriver.py- Contains extensive test scenarios (do not modify)
- Implement private attributes for
_widthand_height - Create properties for
widthandheightwith validation (must be positive)- Raise appropriate ValueError messages as specified in the TODOs
- Important: Check if the value is less than or equal to 0, then raise the error (this ensures consistent behavior)
- Implement read-only properties for
areaandperimeter - Create a
dimensionsproperty with getter, setter, and deleter functionality as described in the TODOs- Important: In the setter, use tuple unpacking syntax:
width, height = dimensions
- Important: In the setter, use tuple unpacking syntax:
Cheat sheet
Advanced property decorators provide sophisticated control over attribute access with computed properties, deleters, and full property management.
Computed Properties:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
@property
def perimeter(self):
return 2 * (self.width + self.height)Full Property with Getter, Setter, and Deleter:
class Temperature:
def __init__(self):
self._temp = 0
@property
def temperature(self):
return self._temp
@temperature.setter
def temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._temp = value
@temperature.deleter
def temperature(self):
print("Resetting temperature to 0")
self._temp = 0Usage:
# Computed properties
rect = Rectangle(5, 3)
print(rect.area) # 15
print(rect.perimeter) # 16
# Full property functionality
temp = Temperature()
temp.temperature = 25 # Uses setter
print(temp.temperature) # Uses getter
del temp.temperature # Uses deleterTry it yourself
from rectangle import Rectangle
# Test case handler
test_case = input()
# Basic functionality test
if test_case == "default_test":
rect = Rectangle(5, 3)
print(f"Width: {rect.width}, Height: {rect.height}")
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}")
# Test the dimensions property
print(f"Dimensions: {rect.dimensions}")
rect.dimensions = (10, 8)
print(f"New area: {rect.area}")
# Test validation
try:
rect.width = -2
except ValueError as e:
print(f"Validation error: {e}")
# Test deleter
del rect.dimensions
print(f"After reset: {rect.dimensions}")
# Test with zero values
elif test_case == "zero_values":
try:
rect = Rectangle(0, 5)
except ValueError as e:
print(f"Error creating rectangle: {e}")
try:
rect = Rectangle(5, 0)
except ValueError as e:
print(f"Error creating rectangle: {e}")
# Test with negative values
elif test_case == "negative_values":
rect = Rectangle(5, 3)
original_dimensions = rect.dimensions
try:
rect.dimensions = (5, -3)
except ValueError as e:
print(f"Error setting dimensions: {e}")
print(f"Dimensions after failed update: {rect.dimensions}")
print(f"Original dimensions preserved: {rect.dimensions == original_dimensions}")
# Test with large values
elif test_case == "large_values":
rect = Rectangle(1000000, 2000000)
print(f"Large rectangle area: {rect.area}")
print(f"Large rectangle perimeter: {rect.perimeter}")
# Test with floating point values
elif test_case == "float_values":
rect = Rectangle(3.5, 2.75)
print(f"Dimensions: {rect.dimensions}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
rect.dimensions = (1.1, 2.2)
print(f"New area with float dimensions: {rect.area}")
# Test with type errors
elif test_case == "type_errors":
try:
rect = Rectangle("5", 3)
except Exception as e:
print(f"Type error during creation: {type(e).__name__}: {e}")
rect = Rectangle(5, 3)
try:
rect.dimensions = 10 # Not a tuple
except Exception as e:
print(f"Type error setting dimensions: {type(e).__name__}: {e}")
# Test multiple property operations
elif test_case == "property_operations":
rect = Rectangle(5, 10)
print(f"Initial - Width: {rect.width}, Height: {rect.height}, Area: {rect.area}")
rect.width = 8
print(f"After width change - Width: {rect.width}, Area: {rect.area}")
rect.height = 6
print(f"After height change - Height: {rect.height}, Area: {rect.area}")
rect.dimensions = (12, 9)
print(f"After dimensions change - Dimensions: {rect.dimensions}, Area: {rect.area}")
del rect.dimensions
print(f"After reset - Dimensions: {rect.dimensions}, Area: {rect.area}")
# Test property validation edge cases
elif test_case == "validation_edge_cases":
rect = Rectangle(5, 3)
try:
rect.width = 0
except ValueError as e:
print(f"Zero width error: {e}")
# Very small positive value should be accepted
rect.height = 0.0001
print(f"Small height accepted: {rect.height}")
print(f"Area with small height: {rect.area}")
# Test multiple rectangles
elif test_case == "multiple_rectangles":
rect1 = Rectangle(5, 3)
rect2 = Rectangle(10, 2)
rect3 = Rectangle(4, 4)
print(f"Rectangle 1 - Area: {rect1.area}, Perimeter: {rect1.perimeter}")
print(f"Rectangle 2 - Area: {rect2.area}, Perimeter: {rect2.perimeter}")
print(f"Rectangle 3 - Area: {rect3.area}, Perimeter: {rect3.perimeter}")
# Modify each rectangle
rect1.width = 7
rect2.height = 5
rect3.dimensions = (6, 6)
print(f"After modifications:")
print(f"Rectangle 1 - Dimensions: {rect1.dimensions}")
print(f"Rectangle 2 - Dimensions: {rect2.dimensions}")
print(f"Rectangle 3 - Dimensions: {rect3.dimensions}")
# Test performance with many operations
elif test_case == "performance_test":
rect = Rectangle(5, 5)
# Perform many property operations
for i in range(1000):
rect.width = i % 10 + 1 # Values 1-10
rect.height = i % 5 + 1 # Values 1-5
area = rect.area # Access computed property
perimeter = rect.perimeter # Access computed property
dims = rect.dimensions # Access property getter
print(f"Final state after 1000 operations:")
print(f"Width: {rect.width}, Height: {rect.height}")
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}")This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesIntroduction to OOPClasses vs ObjectsThe self ParameterMethodsAttributesConstructor Method (__init__)Recap - Simple Calculator4Inheritance
Basic InheritanceThe super() FunctionMethod OverridingMultiple InheritanceMethod Resolution OrderRecap - Employee Hierarchy7Special Methods
Magic Methods IntroductionOperator OverloadingContainer Magic MethodsRecap - Custom List10Design Patterns Part 1
Intro to design patternSingleton PatternFactory PatternObserver PatternStrategy Pattern2Decorators
Introduction to DecoratorsProperty DecoratorStatic Method DecoratorClass Method Decorator5Polymorphism
Method Overriding RevisitedDuck TypingAbstract Classes and MethodsInterface DesignRecap - Shape Calculator8Advanced OOP Concepts
Composition vs InheritanceMixinsStatic and Class MethodsClass DecoratorsContext Managers3Class Properties
Instance vs Class VariablesProperty DecoratorsPrivate AttributesRecap - Bank Account Manager6Encapsulation
Public, Protected, Private MemAccess ModifiersInformation HidingProperty Decorators AdvancedRecap - Student Records System12Project: Library Management
Project OverviewBook and User Classes