setattr() Function Complexity¶
The setattr() function sets the value of a named attribute on an object. It's the programmatic way to assign object attributes dynamically.
Complexity Analysis¶
| Operation | Time | Space | Notes |
|---|---|---|---|
| Direct assignment | O(1) | O(1) | Set in instance dict |
| Property setter call | O(1) | O(1) | Call property set |
| setattr call | O(1) | O(1) | Custom implementation |
| Descriptor protocol | O(1) | O(1) | Data descriptors |
| Total operation | O(1) | O(1) | Hash table insertion |
Basic Usage¶
Set Attribute by Name¶
# O(1) - direct attribute setting
class MyClass:
pass
obj = MyClass()
# Direct assignment - O(1)
obj.value = 42
# Using setattr - O(1)
setattr(obj, 'value', 42)
# Both equivalent, setattr is programmatic
Dynamic Attribute Names¶
# O(1) - programmatic attribute setting
class Config:
pass
config = Config()
# Set attributes dynamically - O(1) per attribute
for key, value in {'host': 'localhost', 'port': 8000}.items():
setattr(config, key, value) # O(1)
print(config.host) # localhost
print(config.port) # 8000
Bulk Initialization¶
# O(n) - set multiple attributes
class Record:
pass
record = Record()
# Initialize from dictionary - O(n)
data = {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}
for key, value in data.items(): # O(n)
setattr(record, key, value) # O(1) per attribute
print(record.name) # Alice
print(record.age) # 30
Complexity Details¶
Direct Assignment to Instance¶
# O(1) - hash table insertion into __dict__
class Simple:
pass
obj = Simple()
# O(1) - insert into obj.__dict__
setattr(obj, 'x', 1)
setattr(obj, 'y', 2)
setattr(obj, 'z', 3)
# Python's attribute storage:
# 1. Get instance.__dict__ (O(1))
# 2. Insert key-value pair (O(1) average)
# Total: O(1)
Property Descriptor Protocol¶
# O(1) - calls property setter if exists
class WithProperty:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = val
obj = WithProperty()
# O(1) - calls setter, not direct assignment
setattr(obj, 'value', 42) # Calls value.setter
Custom setattr¶
# O(1) - calls custom implementation
class TrackedChanges:
def __init__(self):
self._changes = {}
def __setattr__(self, name, value):
# Custom behavior on every assignment
if name == '_changes':
# Avoid recursion for tracking dict
super().__setattr__(name, value)
else:
changes = super().__getattribute__('_changes')
changes[name] = value
super().__setattr__(name, value)
obj = TrackedChanges()
# O(1) - calls __setattr__
setattr(obj, 'x', 1) # Tracked in _changes
print(obj._changes) # {'x': 1}
Performance Patterns¶
Direct vs setattr Performance¶
# Direct assignment - slightly faster O(1)
obj.attr = value # Marginally faster
# setattr - O(1) with slight overhead
setattr(obj, 'attr', value) # Lookup overhead
# Both are O(1), setattr has ~5% overhead for name lookup
# Use direct assignment in performance-critical code
Batch Assignment¶
# O(n) - multiple assignments
class Batch:
pass
obj = Batch()
# Inefficient - multiple function calls
for i in range(1000): # O(n)
setattr(obj, f'attr{i}', i) # O(1) per call
# Better - use __dict__.update
values = {f'attr{i}': i for i in range(1000)} # O(n) - create dict
obj.__dict__.update(values) # O(n) - bulk insert
# Second approach is ~10% faster for large batches
vs Direct dict Manipulation¶
# Direct __dict__ - O(1), bypasses descriptor protocol
class Data:
pass
obj = Data()
# Direct dict assignment - O(1), no descriptor calls
obj.__dict__['x'] = 1
# vs setattr - O(1), respects descriptors
setattr(obj, 'x', 1)
# Direct __dict__ is faster but skips property setters
# Use setattr for proper initialization, __dict__ for performance
Common Use Cases¶
Object Initialization from Dictionary¶
# O(n) - initialize from dict
class User:
def __init__(self, **kwargs):
"""Initialize from keyword arguments - O(n)"""
for key, value in kwargs.items(): # O(n)
setattr(self, key, value) # O(1) per attribute
# O(n) - data = {name, email, age}
user = User(name='Alice', email='alice@ex.com', age=30)
print(user.name) # Alice
print(user.email) # alice@ex.com
Configuration Management¶
# O(n) - apply configuration
class Settings:
"""Base configuration with defaults"""
timeout = 30
retries = 3
debug = False
def apply_config(obj, config_dict):
"""Apply config overrides - O(n)"""
for key, value in config_dict.items(): # O(n)
if hasattr(obj, key): # O(1)
setattr(obj, key, value) # O(1)
settings = Settings()
overrides = {'timeout': 60, 'debug': True}
# O(n) - apply overrides
apply_config(settings, overrides)
print(settings.timeout) # 60
print(settings.debug) # True
Object Cloning¶
# O(n) - copy attributes from source to destination
def clone_object(source, dest):
"""Clone attributes - O(n)"""
for attr in dir(source): # O(n log n)
if not attr.startswith('_'): # O(1)
try:
value = getattr(source, attr) # O(1)
setattr(dest, attr, value) # O(1)
except:
pass # Skip read-only or property attributes
class Original:
x = 1
y = 2
z = 3
class Empty:
pass
orig = Original()
copy = Empty()
clone_object(orig, copy) # O(n) - copy all public attributes
print(copy.x, copy.y) # 1, 2
Data Validation¶
# O(1) - validate on assignment
class ValidatedData:
def __setattr__(self, name, value):
"""Validate data before setting - O(1)"""
if name == 'age':
if not isinstance(value, int) or value < 0:
raise ValueError("Age must be non-negative integer")
super().__setattr__(name, value)
obj = ValidatedData()
setattr(obj, 'age', 30) # O(1) - valid
# setattr(obj, 'age', -5) # Raises ValueError
Builder Pattern¶
# O(n) - builder with fluent interface
class QueryBuilder:
def __init__(self):
self.filters = []
self.limit_value = None
self.offset_value = None
def filter(self, condition):
"""Add filter - O(1)"""
self.filters.append(condition)
return self
def limit(self, value):
"""Set limit - O(1)"""
setattr(self, 'limit_value', value)
return self
def offset(self, value):
"""Set offset - O(1)"""
setattr(self, 'offset_value', value)
return self
def build(self):
return {
'filters': self.filters,
'limit': self.limit_value,
'offset': self.offset_value
}
# O(n) - n = 4 operations
query = (QueryBuilder()
.filter('x > 10')
.filter('y < 20')
.limit(100)
.offset(10)
.build())
Advanced Usage¶
Meta-Programming¶
# O(1) - set descriptor
class Descriptor:
def __set__(self, obj, value):
obj.__dict__['_value'] = value
class MyClass:
prop = Descriptor()
obj = MyClass()
# O(1) - calls descriptor protocol
setattr(obj, 'prop', 42) # Calls Descriptor.__set__
print(obj.__dict__) # {'_value': 42}
Computed Properties¶
# O(1) - set triggers computation
class ComputedObject:
def __init__(self):
self._x = 0
self._y = 0
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
self._invalidate_cache()
@property
def y(self):
return self._y
@y.setter
def y(self, value):
self._y = value
self._invalidate_cache()
def _invalidate_cache(self):
"""Called on any coordinate change - O(1)"""
print("Cache invalidated")
obj = ComputedObject()
# O(1) - triggers _invalidate_cache
setattr(obj, 'x', 10) # Prints "Cache invalidated"
Practical Examples¶
Attribute Mapper¶
# O(n) - map source to destination attributes
def map_attributes(source, dest, mapping):
"""Map attributes with renaming - O(n)"""
for src_attr, dest_attr in mapping.items(): # O(n)
if hasattr(source, src_attr): # O(1)
value = getattr(source, src_attr) # O(1)
setattr(dest, dest_attr, value) # O(1)
class ApiResponse:
user_id = 123
user_name = "Alice"
user_email = "alice@ex.com"
class User:
pass
response = ApiResponse()
user = User()
mapping = {
'user_id': 'id',
'user_name': 'name',
'user_email': 'email'
}
# O(n) - map all attributes
map_attributes(response, user, mapping)
print(user.id) # 123
print(user.name) # Alice
Default Values¶
# O(n) - set defaults for missing attributes
def set_defaults(obj, defaults):
"""Set default values - O(n)"""
for key, value in defaults.items(): # O(n)
if not hasattr(obj, key): # O(1)
setattr(obj, key, value) # O(1)
class Settings:
debug = True
settings = Settings()
defaults = {
'timeout': 30,
'retries': 3,
'debug': False # Already exists, won't override
}
# O(n) - set missing defaults
set_defaults(settings, defaults)
print(settings.timeout) # 30
print(settings.debug) # True (not overridden)
Dynamic Enum Creation¶
# O(n) - create enum-like classes
def create_flags(names):
"""Create flag class dynamically - O(n)"""
flags = type('Flags', (), {})
for i, name in enumerate(names): # O(n)
setattr(flags, name, 1 << i) # O(1) per flag
return flags
# O(n) - n = 5
Flags = create_flags(['READ', 'WRITE', 'EXECUTE', 'DELETE', 'ADMIN'])
print(Flags.READ) # 1
print(Flags.WRITE) # 2
print(Flags.ADMIN) # 16
Edge Cases¶
Property Without Setter¶
# O(1) - raises error if property has no setter
class ReadOnly:
@property
def value(self):
return 42
obj = ReadOnly()
# Raises AttributeError - property has no setter
try:
setattr(obj, 'value', 100) # O(1) but raises
except AttributeError:
print("Cannot set read-only property")
slots¶
# O(1) - works with __slots__
class Slotted:
__slots__ = ['x', 'y']
obj = Slotted()
# O(1) - set slot attribute
setattr(obj, 'x', 1)
setattr(obj, 'y', 2)
# O(1) - faster than __dict__ objects
# But cannot add new attributes
try:
setattr(obj, 'z', 3) # AttributeError - no slot for z
except AttributeError:
print("No slot for 'z'")
Immutable Objects¶
# O(1) - cannot set attributes on immutable types
class Immutable:
__slots__ = [] # No slots for instances
obj = Immutable()
# Raises AttributeError
try:
setattr(obj, 'x', 1) # O(1) but raises
except AttributeError:
print("Cannot set attributes on this object")
Performance Considerations¶
Avoiding setattr Overhead¶
# Direct __dict__ assignment - O(1) faster
class FastInit:
def __init__(self, **kwargs):
# Bypass __setattr__ by direct dict assignment
self.__dict__.update(kwargs) # O(n) - single call
# vs with setattr
class SlowInit:
def __init__(self, **kwargs):
# Calls __setattr__ for each attribute
for k, v in kwargs.items():
setattr(self, k, v) # O(1) each, but more calls
# __dict__.update is ~30% faster for bulk initialization
Batch vs Individual Updates¶
# Individual updates - O(n) but multiple operations
obj = object.__new__(type('Obj', (), {}))
for i in range(100):
setattr(obj, f'attr{i}', i) # 100 function calls
# Batch update - O(n) but single operation
obj.__dict__ = {f'attr{i}': i for i in range(100)}
# Batch is ~20% faster
Best Practices¶
✅ Do:
- Use
setattr()for dynamic attribute assignment - Use
setattr()to respect property setters - Use
__dict__.update()for bulk initialization - Cache attribute names if setting repeatedly
- Use descriptors for computed properties
❌ Avoid:
- Using
setattr()in tight O(n) loops repeatedly - Assuming all attributes can be set (check hasattr first)
- Direct
__dict__assignment when descriptors should be called - Expensive operations in
__setattr__(consider caching) - Setting private/internal attributes (use setattr carefully)
Related Functions¶
- getattr() - Get attribute value
- hasattr() - Check attribute existence
- delattr() - Delete attribute
- dir() - List attributes
- vars() - Get dict
Version Notes¶
- Python 2.x:
setattr()available, basic functionality - Python 3.x: Same behavior, optimized in CPython
- All versions: Returns None, respects descriptor protocol