11: Custom Attribute Access¶
Summary¶
The descriptor object can customize to any attribute via the __get__, __set__ and __delete__ functions which are automatically called on attribute access or deletion
- If a descriptor defines __get__ and __set__ then it is a data descriptor
If only __get__ is defined it is a non-data descriptor
Data descriptors cannot be overridden by a definition in the instance, whereas non-data descriptors can be
__getattribute__ is called for every attribute access and if you override it then you override every attribute access
If __getattribute__ fails to find an attribute then it automatically calls __getattr__, if it is defined
__setattr__ is called every time you attempt an assignment to an attribute
You can use __getattr__ to implement default attributes and methods
You can use del or delattr to delete an attribute, but in either case __delattr__ is called, if it is defined
The __dir__ magic method can be used to define a custom dir command
Slots are just a more efficient way of implementing attributes using a descriptor
- Another way of accessing attributes is via the index or key operator, []
This is customized using __getitem__ and __setitem__
There are also a range of addirtional magic methods that can make attribute access look more like a collection
Program¶
"""
Custom Attribute Access Demonstration
Covers:
- Descriptors (__get__, __set__, __delete__)
- Data vs Non-data descriptors
- __getattribute__ and __getattr__
- __setattr__ and __delattr__
- __dir__
- __slots__
- __getitem__ and __setitem__
"""
# -------------------------------
# DESCRIPTORS
# -------------------------------
class DataDescriptor:
"""Defines both __get__ and __set__ → DATA descriptor"""
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
print(f"[DataDescriptor __get__] Getting {self.name}")
return instance.__dict__.get(self.name, None)
def __set__(self, instance, value):
print(f"[DataDescriptor __set__] Setting {self.name} = {value}")
instance.__dict__[self.name] = value
def __delete__(self, instance):
print(f"[DataDescriptor __delete__] Deleting {self.name}")
del instance.__dict__[self.name]
class NonDataDescriptor:
"""Defines only __get__ → NON-DATA descriptor"""
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
print("[NonDataDescriptor __get__] Accessed")
return self.value
# -------------------------------
# MAIN CLASS WITH MAGIC METHODS
# -------------------------------
class Demo:
# descriptors
data = DataDescriptor("data")
non_data = NonDataDescriptor("default")
# slots (efficient attribute storage)
__slots__ = ['slot_attr', '__dict__']
def __init__(self):
self.slot_attr = "I am in slots"
# called for EVERY attribute access
def __getattribute__(self, name):
print(f"[__getattribute__] Accessing: {name}")
try:
return super().__getattribute__(name)
except AttributeError:
# fallback to __getattr__
return self.__getattr__(name)
# called ONLY if attribute not found
def __getattr__(self, name):
print(f"[__getattr__] {name} not found, returning default")
return f"default_{name}"
# called on assignment
def __setattr__(self, name, value):
print(f"[__setattr__] Setting {name} = {value}")
super().__setattr__(name, value)
# called on deletion
def __delattr__(self, name):
print(f"[__delattr__] Deleting {name}")
super().__delattr__(name)
# customize dir()
def __dir__(self):
return ["custom_attr1", "custom_attr2", "data", "non_data"]
# collection-style access
def __getitem__(self, key):
print(f"[__getitem__] Getting key: {key}")
return self.__dict__.get(key, None)
def __setitem__(self, key, value):
print(f"[__setitem__] Setting key: {key} = {value}")
self.__dict__[key] = value
# -------------------------------
# DEMONSTRATION
# -------------------------------
if __name__ == "__main__":
obj = Demo()
print("\n--- DATA DESCRIPTOR ---")
obj.data = 10 # triggers __set__
print(obj.data) # triggers __get__
del obj.data # triggers __delete__
print("\n--- NON-DATA DESCRIPTOR ---")
print(obj.non_data) # descriptor used
obj.non_data = "override"
print(obj.non_data) # instance overrides descriptor
print("\n--- __getattr__ FALLBACK ---")
print(obj.missing_attr)
print("\n--- __setattr__ ---")
obj.new_attr = 42
print("\n--- __delattr__ ---")
del obj.new_attr
print("\n--- __dir__ ---")
print(dir(obj))
print("\n--- __slots__ ---")
print(obj.slot_attr)
print("\n--- COLLECTION ACCESS ---")
obj["key"] = "value"
print(obj["key"])
Program Output¶
[__setattr__] Setting slot_attr = I am in slots
--- DATA DESCRIPTOR ---
[__setattr__] Setting data = 10
[DataDescriptor __set__] Setting data = 10
[__getattribute__] Accessing: __dict__
[__getattribute__] Accessing: data
[DataDescriptor __get__] Getting data
[__getattribute__] Accessing: __dict__
10
[__delattr__] Deleting data
[DataDescriptor __delete__] Deleting data
[__getattribute__] Accessing: __dict__
--- NON-DATA DESCRIPTOR ---
[__getattribute__] Accessing: non_data
[NonDataDescriptor __get__] Accessed
default
[__setattr__] Setting non_data = override
[__getattribute__] Accessing: non_data
override
--- __getattr__ FALLBACK ---
[__getattribute__] Accessing: missing_attr
[__getattribute__] Accessing: __getattr__
[__getattr__] missing_attr not found, returning default
default_missing_attr
--- __setattr__ ---
[__setattr__] Setting new_attr = 42
--- __delattr__ ---
[__delattr__] Deleting new_attr
--- __dir__ ---
['custom_attr1', 'custom_attr2', 'data', 'non_data']
--- __slots__ ---
[__getattribute__] Accessing: slot_attr
I am in slots
--- COLLECTION ACCESS ---
[__setitem__] Setting key: key = value
[__getattribute__] Accessing: __dict__
[__getitem__] Getting key: key
[__getattribute__] Accessing: __dict__
valuec