property() Decorator Complexity¶
The property() decorator allows you to define custom getter, setter, and deleter methods for class attributes. It enables attribute-like access with custom logic.
Complexity Analysis¶
| Operation | Time | Space | Notes |
|---|---|---|---|
| Create property | O(1) | O(1) | Decorator application |
| Get value | O(1) | O(1) | Call getter function |
| Set value | O(1) | O(1) | Call setter function |
| Delete value | O(1) | O(1) | Call deleter function |
| Descriptor lookup | O(d) | O(1) | d = MRO depth |
| Total operation | O(d) | O(1) | MRO traversal may occur |
Basic Usage¶
Simple Getter¶
# O(1) - define read-only property
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get radius - O(1)"""
return self._radius
# O(1) - access like attribute
circle = Circle(5)
r = circle.radius # 5 (not a method call)
# Cannot set - read-only
# circle.radius = 10 # AttributeError
Getter and Setter¶
# O(1) - define read-write property
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Get temperature - O(1)"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature - O(1)"""
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
# O(1) - get and set
temp = Temperature(20)
print(temp.celsius) # 20
temp.celsius = 25 # O(1) - calls setter
print(temp.celsius) # 25
Computed Property¶
# O(1) - property computes value
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
"""Calculate area - O(1)"""
return self.width * self.height
# O(1) - computed on access
rect = Rectangle(5, 10)
a = rect.area # 50 (computed, not stored)
# Change dimensions
rect.width = 6
a = rect.area # 60 (recomputed)
Getter, Setter, and Deleter¶
# O(1) - complete property control
class Data:
def __init__(self):
self._value = None
@property
def value(self):
"""Get value - O(1)"""
print("Getting value")
return self._value
@value.setter
def value(self, val):
"""Set value - O(1)"""
print(f"Setting value to {val}")
self._value = val
@value.deleter
def value(self):
"""Delete value - O(1)"""
print("Deleting value")
del self._value
# O(1) - use property
d = Data()
d.value = 42 # Prints: Setting value to 42
v = d.value # Prints: Getting value, returns 42
del d.value # Prints: Deleting value
Complexity Details¶
Descriptor Protocol¶
# O(1) - property uses descriptor protocol
class WithProperty:
@property
def value(self):
return 42
# When accessing:
# 1. Lookup 'value' in class dict - O(1)
# 2. Found descriptor (property object)
# 3. Call property.__get__() - O(1)
# 4. Calls getter function - O(1)
obj = WithProperty()
val = obj.value # O(1)
MRO Traversal¶
# O(d) - inherited properties
class Parent:
@property
def value(self):
return "parent"
class Child(Parent):
pass
# O(d) - lookup via MRO
obj = Child()
val = obj.value # O(d) - traverse MRO to find property
Property Override¶
# O(d) - child can override property
class Parent:
@property
def value(self):
return "parent"
class Child(Parent):
@property
def value(self):
return "child"
# O(d) - finds Child property first
parent_val = Parent().value # "parent"
child_val = Child().value # "child"
Performance Patterns¶
Property vs Direct Attribute¶
# Direct attribute - O(1)
class Direct:
def __init__(self):
self.value = 42
# Property - O(1) + function call overhead
class WithProperty:
def __init__(self):
self._value = 42
@property
def value(self):
return self._value
# Both O(1), but property has ~5% overhead
direct = Direct()
val1 = direct.value # O(1)
with_prop = WithProperty()
val2 = with_prop.value # O(1) + overhead
Property vs Method Call¶
# Method - O(1) with explicit call
class WithMethod:
def __init__(self):
self._value = 42
def get_value(self):
return self._value
# Property - O(1) with implicit call
class WithProperty:
def __init__(self):
self._value = 42
@property
def value(self):
return self._value
# Both O(1), property looks cleaner
obj1 = WithMethod()
val1 = obj1.get_value() # Explicit call
obj2 = WithProperty()
val2 = obj2.value # Attribute access
Caching in Properties¶
# O(1) with caching - compute once
class Cached:
def __init__(self):
self._computed = None
@property
def value(self):
"""Compute once and cache - O(1)"""
if self._computed is None:
self._computed = expensive_computation()
return self._computed
def expensive_computation(self):
# ... slow operation ...
return 42
# vs recompute every time - O(f) where f = computation time
class NoCache:
@property
def value(self):
"""Recompute every time"""
return expensive_computation()
# Caching significantly improves performance
Common Use Cases¶
Validation on Set¶
# O(1) - validate input
class Person:
def __init__(self, age):
self._age = None
self.age = age # Calls setter
@property
def age(self):
"""Get age - O(1)"""
return self._age
@age.setter
def age(self, value):
"""Set age with validation - O(1)"""
if not isinstance(value, int) or value < 0:
raise ValueError("Age must be non-negative integer")
self._age = value
# O(1) - automatic validation
p = Person(30)
p.age = 35 # Valid
try:
p.age = -5 # Invalid - raises ValueError
except ValueError as e:
print(e)
Computed Properties¶
# O(1) - compute related value
class Distance:
def __init__(self, kilometers):
self.km = kilometers
@property
def miles(self):
"""Convert to miles - O(1)"""
return self.km * 0.621371
# O(1) - accessed like attribute
d = Distance(10)
miles = d.miles # 6.21371
Lazy Loading¶
# O(1) first access, O(n) computation
class Document:
def __init__(self, filename):
self.filename = filename
self._content = None
@property
def content(self):
"""Load on first access - O(1) or O(n)"""
if self._content is None:
with open(self.filename) as f:
self._content = f.read() # O(n) - load file
return self._content # O(1) - return cached
# O(1) - first call loads file
# O(1) - subsequent calls return cached
doc = Document('large_file.txt')
text1 = doc.content # O(n) - loads
text2 = doc.content # O(1) - cached
Read-Only Properties¶
# O(1) - immutable attributes
class ImmutablePoint:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
"""Get x - O(1), read-only"""
return self._x
@property
def y(self):
"""Get y - O(1), read-only"""
return self._y
# O(1) - get values
p = ImmutablePoint(1, 2)
x = p.x # 1
# Cannot set - no setter defined
# p.x = 5 # AttributeError
Type Conversion¶
# O(1) - convert on access
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Get Celsius - O(1)"""
return self._celsius
@property
def fahrenheit(self):
"""Get Fahrenheit - O(1)"""
return self._celsius * 9/5 + 32
@property
def kelvin(self):
"""Get Kelvin - O(1)"""
return self._celsius + 273.15
# O(1) - convert automatically
temp = Temperature(20)
c = temp.celsius # 20
f = temp.fahrenheit # 68.0
k = temp.kelvin # 293.15
Advanced Usage¶
Property with Side Effects¶
# O(1) - setter with side effects
class Volume:
def __init__(self, value):
self._value = value
self.changed = False
@property
def value(self):
"""Get value - O(1)"""
return self._value
@value.setter
def value(self, new_value):
"""Set value and track change - O(1)"""
if new_value != self._value:
self.changed = True
self._value = new_value
# O(1) - track changes
vol = Volume(100)
vol.value = 150 # Sets changed = True
print(vol.changed) # True
Property Inheritance¶
# O(d) - override property in subclass
class Parent:
@property
def name(self):
return "Parent"
class Child(Parent):
@property
def name(self):
return "Child"
# O(d) - finds correct property
parent = Parent()
child = Child()
print(parent.name) # "Parent"
print(child.name) # "Child"
Dynamic Properties¶
# O(1) - add properties dynamically
class Dynamic:
def __init__(self):
self._data = {}
def add_property(self, name, getter, setter=None):
"""Add property dynamically - O(1)"""
prop = property(getter, setter)
setattr(self.__class__, name, prop)
# Create instance
obj = Dynamic()
# Add property dynamically
obj.add_property(
'value',
getter=lambda self: self._data.get('value', 0)
)
# Use property
obj.value = 42
Practical Examples¶
Account Balance with Limits¶
# O(1) - property with validation and limits
class BankAccount:
def __init__(self, initial_balance):
self._balance = initial_balance
@property
def balance(self):
"""Get balance - O(1)"""
return self._balance
@balance.setter
def balance(self, amount):
"""Set balance with validation - O(1)"""
if amount < 0:
raise ValueError("Balance cannot be negative")
if amount > 1_000_000:
raise ValueError("Balance exceeds limit")
self._balance = amount
@balance.deleter
def balance(self):
"""Reset balance - O(1)"""
self._balance = 0
# O(1) - use account
account = BankAccount(100)
print(account.balance) # 100
account.balance = 500 # Valid
print(account.balance) # 500
del account.balance # Reset to 0
print(account.balance) # 0
Configuration Management¶
# O(1) - validate configuration
class Config:
def __init__(self):
self._debug = False
self._timeout = 30
@property
def debug(self):
"""Get debug flag - O(1)"""
return self._debug
@debug.setter
def debug(self, value):
"""Set debug - O(1)"""
if not isinstance(value, bool):
raise TypeError("debug must be boolean")
self._debug = value
@property
def timeout(self):
"""Get timeout - O(1)"""
return self._timeout
@timeout.setter
def timeout(self, seconds):
"""Set timeout with bounds - O(1)"""
if not (1 <= seconds <= 300):
raise ValueError("timeout must be 1-300 seconds")
self._timeout = seconds
# O(1) - validated configuration
config = Config()
config.debug = True
config.timeout = 60
User Authentication¶
# O(1) - property for authenticated user
class User:
def __init__(self, username, password_hash):
self.username = username
self._password_hash = password_hash
self._authenticated = False
@property
def authenticated(self):
"""Check authentication - O(1)"""
return self._authenticated
def authenticate(self, password):
"""Authenticate - O(1)"""
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
if password_hash == self._password_hash:
self._authenticated = True
return self._authenticated
@authenticated.setter
def authenticated(self, value):
"""Prevent direct setting - O(1)"""
raise AttributeError("Cannot set authenticated directly")
# O(1) - use authentication
user = User("alice", "hash123")
is_auth = user.authenticated # False
user.authenticate("mypassword") # Authenticates if password matches
Edge Cases¶
Property Can't Have Same Name as init Param¶
# O(1) - attribute name matters
class Good:
def __init__(self, value):
self._value = value # Different name
@property
def value(self):
return self._value
# O(1) - works fine
obj = Good(42)
val = obj.value # 42
Deleter Without Setter¶
# O(1) - can define deleter without setter
class OnlyDelete:
def __init__(self):
self.value = 42
@property
def value(self):
return self._value if hasattr(self, '_value') else None
@value.deleter
def value(self):
if hasattr(self, '_value'):
del self._value
# O(1) - delete works
obj = OnlyDelete()
del obj.value
Property on Module¶
# O(1) - properties in classes, not modules
# (modules don't support property directly in Python 3.6+)
# But can be emulated with __getattr__
import sys
class ModuleProxy:
def __init__(self, module):
self.module = module
@property
def version(self):
return "1.0.0"
# Access: ModuleProxy(sys).version
Performance Considerations¶
Avoiding Property Overhead¶
# If performance critical, use direct attribute:
class Fast:
def __init__(self):
self.value = 42
# Slightly faster than property (no function call)
obj = Fast()
val = obj.value # Very fast
# vs property (function call overhead)
class WithProperty:
def __init__(self):
self._value = 42
@property
def value(self):
return self._value
# Overhead is minimal (~5%), only matters in extreme cases
Caching Computed Properties¶
# O(1) - cache expensive computation
class Cached:
def __init__(self):
self._cached_value = None
@property
def value(self):
"""Compute once - O(1) after first access"""
if self._cached_value is None:
self._cached_value = expensive_computation()
return self._cached_value
def expensive_computation(self):
return sum(range(1_000_000))
# First access does computation, subsequent accesses are fast
Best Practices¶
✅ Do:
- Use for read-only computed attributes
- Use to validate input in setters
- Use to implement lazy loading
- Use to provide clean attribute-like interface
- Document properties in docstrings
- Make getters fast (no I/O)
❌ Avoid:
- Complex logic in property methods (keep simple)
- I/O operations in getters (causes performance issues)
- Properties with unexpected side effects
- Using property when method is clearer
- Expensive computations without caching
- Hiding errors in property methods
Related Functions¶
- classmethod() - Decorator for class methods
- staticmethod() - Decorator for static methods
- type() - Get object type
- hasattr() - Check attribute existence
- getattr() - Get attribute value
Version Notes¶
- Python 2.x:
@propertyavailable, same behavior - Python 3.x: Same behavior, optimized in CPython
- All versions: Uses descriptor protocol for efficiency