Keywords: Python | __getattr__ | __getattribute__
Abstract: This article provides an in-depth exploration of the differences and application scenarios between Python's __getattr__ and __getattribute__ special methods. Through detailed analysis of invocation timing, implementation mechanisms, and common pitfalls, combined with concrete code examples, it clarifies that __getattr__ is called only as a fallback when attributes are not found, while __getattribute__ intercepts all attribute accesses. The article also discusses how to avoid infinite recursion, the impact of new-style vs old-style classes, and best practice choices in actual development.
Core Concepts and Invocation Mechanisms
In Python object-oriented programming, __getattr__ and __getattribute__ are two special methods for customizing attribute access, but they differ fundamentally in their invocation timing and behavior.
__getattr__ is called only when standard attribute lookup fails, i.e., when the attempted object attribute is not found in the instance dictionary, class dictionary, or parent class chain. This makes it ideal for implementing fallback logic for missing attributes, such as dynamically creating default values or raising customized exceptions.
In contrast, __getattribute__ is triggered on every attribute access, regardless of whether the attribute exists. It completely takes over the attribute lookup process, so it must be implemented carefully to avoid infinite recursion. Typically, its implementation needs to call the base class's __getattribute__ method to perform the actual attribute resolution.
Code Examples and Behavioral Analysis
The following example demonstrates the basic usage of __getattr__:
class DynamicAttributes:
def __init__(self):
self.defined_attr = "defined attribute"
def __getattr__(self, name):
print(f"Calling __getattr__, requested attribute: {name}")
value = f"dynamically generated {name}"
setattr(self, name, value)
return value
obj = DynamicAttributes()
print(obj.defined_attr) # Output: defined attribute
print(obj.undefined_attr) # Output: Calling __getattr__, requested attribute: undefined_attr dynamically generated undefined_attrIn this code, accessing the existing defined_attr does not trigger __getattr__, while accessing undefined_attr triggers the method and dynamically creates the attribute.
For __getattribute__, consider this implementation:
class ControlledAccess:
def __init__(self):
self.public_data = "public data"
self._private_data = "private data"
def __getattribute__(self, name):
if name.startswith('_'):
raise AttributeError(f"Access denied to private attribute: {name}")
return super().__getattribute__(name)
obj = ControlledAccess()
print(obj.public_data) # Output: public data
print(obj._private_data) # Raises AttributeError: Access denied to private attribute: _private_dataHere, __getattribute__ intercepts all attribute accesses and enforces access control for attributes starting with an underscore.
Key Techniques to Avoid Infinite Recursion
Directly accessing self.attribute or self.__dict__['attribute'] within __getattribute__ causes recursive calls to itself, leading to RecursionError. The correct approach is to use super().__getattribute__(name) or object.__getattribute__(self, name) to delegate to the base class's lookup mechanism.
Incorrect example:
class IncorrectImplementation:
def __getattribute__(self, name):
# Incorrect: direct access to self.__dict__ causes recursion
return self.__dict__[name] # Raises RecursionErrorCorrect implementation:
class CorrectImplementation:
def __init__(self):
self.value = 42
def __getattribute__(self, name):
# Correct: access attribute via base class method
attr_value = super().__getattribute__(name)
print(f"Accessing attribute {name}: {attr_value}")
return attr_value
obj = CorrectImplementation()
print(obj.value) # Output: Accessing attribute value: 42 42Method Coordination and Exception Handling
When both __getattribute__ and __getattr__ are defined in a class, __getattribute__ is called first. If __getattribute__ raises an AttributeError exception, Python catches it and subsequently calls __getattr__.
Example:
class CombinedMethods:
def __init__(self):
self.existing = "existing attribute"
def __getattribute__(self, name):
if name == "forbidden":
raise AttributeError("Attribute access forbidden")
return super().__getattribute__(name)
def __getattr__(self, name):
return f"fallback value: {name}"
obj = CombinedMethods()
print(obj.existing) # Output: existing attribute
print(obj.forbidden) # Output: fallback value: forbidden
print(obj.nonexistent) # Output: fallback value: nonexistentIn this case, accessing the forbidden attribute causes __getattribute__ to raise AttributeError, triggering __getattr__ to provide a fallback value.
Impact of New-Style vs Old-Style Classes
In Python 2, new-style classes (explicitly inheriting from object) support __getattribute__, while old-style classes do not. In Python 3, all classes are new-style, so this distinction is no longer relevant. Current development should always use new-style classes.
Practical Application Scenarios and Recommendations
Scenarios for using __getattr__:
- Implementing lazy attribute loading or computed attributes
- Providing default values for non-existent attributes
- Implementing dynamic attribute generation, such as in proxy patterns
Scenarios for using __getattribute__:
- Enforcing strict attribute access control
- Logging all attribute access
- Implementing global interception and modification of attribute access
In most cases, __getattr__ is the safer and more intuitive choice, as it does not interfere with the normal attribute resolution flow. Consider using __getattribute__ only when complete control over the attribute access mechanism is necessary, and always be mindful of recursion avoidance.