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