Difficulty: Intermediate
What is the @property decorator in Python? How does it relate to the descriptor protocol?
@property is a built-in decorator that turns a method into a managed attribute, allowing getter, setter, and deleter logic with clean dot-notation access.
Benefits: - Start with a simple attribute, add validation/computation later without changing the public API - Computed properties (derived from other attributes) - Validation on assignment
Descriptor Protocol: any object that implements __get__, __set__, or __delete__ is a descriptor. @property is a built-in descriptor. You can create custom descriptors for reusable attribute behaviors.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius # Private backing attribute
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero!')
self._celsius = value
@celsius.deleter
def celsius(self):
del self._celsius
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
# Read-only computed property
@property
def kelvin(self):
return self._celsius + 273.15
t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
print(t.kelvin) # 298.15
t.celsius = 100
print(t.fahrenheit) # 212.0
try:
t.celsius = -500
except ValueError as e:
print(e)
The @celsius.setter validates input before storing. fahrenheit and kelvin are computed properties - always derived from _celsius.
class Validated:
"""Reusable descriptor for validated numeric attributes."""
def __init__(self, min_val, max_val):
self.min_val = min_val
self.max_val = max_val
self.name = None # Set by __set_name__
def __set_name__(self, owner, name):
self.name = name # Called when class is created
def __get__(self, obj, objtype=None):
if obj is None:
return self # Accessed from class, not instance
return getattr(obj, f'_{self.name}', None)
def __set__(self, obj, value):
if not (self.min_val <= value <= self.max_val):
raise ValueError(
f'{self.name} must be between {self.min_val} and {self.max_val}'
)
setattr(obj, f'_{self.name}', value)
class Employee:
age = Validated(18, 65)
salary = Validated(0, 1_000_000)
def __init__(self, name, age, salary):
self.name = name
self.age = age # Calls Validated.__set__
self.salary = salary # Calls Validated.__set__
e = Employee('Alice', 30, 75000)
print(e.age, e.salary)
try:
e.age = 15 # ValueError
except ValueError as e:
print(e)
__set_name__ is called when the class is defined, passing the attribute name. Descriptors enable reusable validation across multiple class attributes.
@property, getter, setter, deleter, Descriptor Protocol