getattribute¶
Fundamentals¶
1. Definition¶
The __getattribute__ method is automatically called by Python for every attribute access on an object:
obj.attr # Triggers obj.__getattribute__('attr')
getattr(obj, 'attr') # Also triggers obj.__getattribute__('attr')
2. Method Signature¶
def __getattribute__(self, name):
# name is a string
# Must return a value or raise AttributeError
pass
3. Universal Hook¶
Key characteristic: __getattribute__ is called for every attribute access, even:
- Instance attributes
- Class attributes
- Methods
- Properties
- Dunder methods (like __dict__)
Basic Implementation¶
1. Simple Override¶
class MyClass:
def __init__(self):
self.value = 42
def __getattribute__(self, name):
print(f"Accessing attribute: {name}")
return super().__getattribute__(name)
obj = MyClass()
x = obj.value
# Output: Accessing attribute: value
# Returns: 42
2. Without super()¶
class MyClass:
def __getattribute__(self, name):
print(f"Getting: {name}")
return object.__getattribute__(self, name)
3. Must Return or Raise¶
def __getattribute__(self, name):
if name == 'secret':
raise AttributeError("Access denied")
return super().__getattribute__(name)
Avoiding Recursion¶
1. The Problem¶
class Broken:
def __getattribute__(self, name):
# ❌ INFINITE RECURSION!
return self.name # Calls __getattribute__ again!
2. Correct Approaches¶
Use super():
class Correct:
def __getattribute__(self, name):
return super().__getattribute__(name)
Use object.__getattribute__:
class Correct:
def __getattribute__(self, name):
return object.__getattribute__(self, name)
Direct dict access:
class Correct:
def __getattribute__(self, name):
return object.__getattribute__(self, '__dict__')[name]
3. Why Recursion Happens¶
def __getattribute__(self, name):
value = self.something # Calls __getattribute__('something')
# Which calls __getattribute__('something')
# Which calls __getattribute__('something')
# ... forever!
Practical Examples¶
1. Logging Access¶
class LoggedAccess:
def __init__(self):
self.data = {'x': 1, 'y': 2}
def __getattribute__(self, name):
print(f"[LOG] Accessing: {name}")
return super().__getattribute__(name)
obj = LoggedAccess()
print(obj.data)
# [LOG] Accessing: data
# {'x': 1, 'y': 2}
2. Access Control¶
class Restricted:
def __init__(self):
self._private = "secret"
self.public = "visible"
def __getattribute__(self, name):
if name.startswith('_') and not name.startswith('__'):
raise AttributeError(f"Cannot access private attribute: {name}")
return super().__getattribute__(name)
obj = Restricted()
print(obj.public) # ✅ "visible"
# print(obj._private) # ❌ AttributeError
3. Lazy Loading¶
class LazyLoader:
def __init__(self):
self._loaded = False
self._data = None
def __getattribute__(self, name):
if name == 'data':
loaded = super().__getattribute__('_loaded')
if not loaded:
print("Loading data...")
object.__setattr__(self, '_data', [1, 2, 3, 4, 5])
object.__setattr__(self, '_loaded', True)
return super().__getattribute__('_data')
return super().__getattribute__(name)
obj = LazyLoader()
print(obj.data) # Loading data... [1, 2, 3, 4, 5]
print(obj.data) # [1, 2, 3, 4, 5] (no loading)
Interaction with Properties¶
1. Properties Still Work¶
class Example:
def __init__(self):
self._value = 42
@property
def value(self):
return self._value * 2
def __getattribute__(self, name):
print(f"Intercepting: {name}")
return super().__getattribute__(name)
obj = Example()
print(obj.value)
# Intercepting: value
# 84 (property getter was called)
2. Order of Operations¶
obj.value
↓
__getattribute__('value') called
↓
super().__getattribute__('value')
↓
Finds property descriptor in class
↓
Calls property's __get__ method
↓
Returns result
3. Blocking Properties¶
class BlockProperty:
@property
def value(self):
return 42
def __getattribute__(self, name):
if name == 'value':
return 100 # Bypass property!
return super().__getattribute__(name)
obj = BlockProperty()
print(obj.value) # 100 (property never called)
Advanced Patterns¶
1. Attribute Proxy¶
class Proxy:
def __init__(self, obj):
object.__setattr__(self, '_obj', obj)
def __getattribute__(self, name):
if name == '_obj':
return object.__getattribute__(self, name)
obj = object.__getattribute__(self, '_obj')
return getattr(obj, name)
class Target:
def __init__(self):
self.value = 42
proxy = Proxy(Target())
print(proxy.value) # 42
2. Attribute Mapping¶
class AttributeMapper:
def __init__(self):
self._mapping = {
'old_name': 'new_name',
'deprecated': 'current'
}
self.new_name = "value"
self.current = "data"
def __getattribute__(self, name):
mapping = super().__getattribute__('_mapping')
if name in mapping:
name = mapping[name]
return super().__getattribute__(name)
obj = AttributeMapper()
print(obj.old_name) # "value"
print(obj.deprecated) # "data"
3. Counting Access¶
class AccessCounter:
def __init__(self):
object.__setattr__(self, '_counts', {})
self.data = [1, 2, 3]
def __getattribute__(self, name):
if name not in ('_counts', '__dict__', '__class__'):
counts = object.__getattribute__(self, '_counts')
counts[name] = counts.get(name, 0) + 1
return super().__getattribute__(name)
def get_counts(self):
return self._counts
obj = AccessCounter()
obj.data
obj.data
obj.data
print(obj.get_counts()) # {'data': 3}
When to Use¶
1. Good Use Cases¶
✅ Debugging and logging - Track all attribute access ✅ Proxies and wrappers - Delegate to another object ✅ Access control - Enforce security policies ✅ Attribute translation - Map old names to new names
2. Avoid When¶
❌ Simple attribute access - Use normal attributes
❌ Validation only - Use @property instead
❌ Missing attributes - Use __getattr__ instead
❌ Performance critical - Adds overhead
3. Comparison¶
| Scenario | Use |
|---|---|
| All attribute access | __getattribute__ |
| Missing attributes only | __getattr__ |
| Specific attributes | @property |
| Simple storage | Plain attributes |
Common Mistakes¶
1. Infinite Recursion¶
# ❌ BAD
def __getattribute__(self, name):
if self.ready: # Calls __getattribute__!
return self.value
Fix:
# ✅ GOOD
def __getattribute__(self, name):
ready = super().__getattribute__('ready')
if ready:
return super().__getattribute__('value')
2. Forgetting to Return¶
# ❌ BAD
def __getattribute__(self, name):
print(f"Accessing {name}")
# Forgot to return!
# Returns None for everything!
3. Breaking Built-ins¶
# ❌ BAD
def __getattribute__(self, name):
return "always this" # Breaks __dict__, __class__, etc.