Chapter Eleven Python Program/Notes

Chapter 11 - Custom Attribute Access

  • The descriptor object creates a special type of attribute with __get__(), __set__(), or delete() defined.

  • It’s just a property implemented by a special type of descriptor.

  • Man I dunno.

  • Getters get stuff (duh).

  • Setters change the value of a thingy.

  • Attributes are really strong, and slots lets them be a bit even more optimized.

  • Slots are kind of a bit more nuanced then I care to go into, but in some cases where speed matters more than customization they can be ‘better’ than __dict__.

  • The special attribute __slots__ allows you to explicitly state which instance attributes you expect your object instances to have, with the expected results.

  • __setattr__ is called every time you attempt an assignment to an attribute.

  • __getattribute__ is a pretty cool getter, but if you override it, you override every attribute access.

  • Data descriptors cannot be overridden by a defection in the instance, whereas non-data descriptors can be.

Example Program

import time
# 1. A Data Descriptor (has __set__)
# It cannot be overridden by an instance dictionary.
class ValidSpeed:
    def __get__(self, instance, owner):
        return instance.__dict__.get('_speed', 0)

    def __set__(self, instance, value):
        if value < 0 or value > 120:
            raise ValueError("Speed must be between 0 and 120!")
        instance.__dict__['_speed'] = value

# 2. A Non-Data Descriptor (only has __get__)
# This CAN be overridden by an instance attribute.
class EngineStatus:
    def __get__(self, instance, owner):
        return "The engine is purring."

class SmartCar:
    # 3. Slots: Optimization over __dict__
    # We are restricting attributes to save memory and increase speed.
    __slots__ = ('_speed', 'make', 'model', '__dict__')

    speed = ValidSpeed()    # Descriptor object
    status = EngineStatus() # Non-data descriptor

    def __init__(self, make, model):
        self.make = make
        self.model = model

    # 4. Property: Just a fancy descriptor wrapper
    @property
    def description(self):
        return f"This is a {self.make} {self.model}."

    # 5. __setattr__: Called every time you assign something
    def __setattr__(self, name, value):
        print(f"--- LOG: Setting {name} to {value} ---")
        super().__setattr__(name, value)

    # 6. __getattribute__: The "Nuclear" Getter
    # Overrides EVERY access. Be careful with infinite recursion!
    def __getattribute__(self, name):
        # We use super() to avoid calling ourselves forever
        val = super().__getattribute__(name)
        if name == 'speed':
            print("--- LOG: Accessing the speed sensor ---")
        return val

# --- Let's see it in action ---

car = SmartCar("Tesla", "Model S")

# Testing __setattr__
car.speed = 65

# Testing __getattribute__ and Descriptor
print(f"Current Speed: {car.speed}")

# Testing Data vs Non-Data Override
car.status = "Broken" # Overrides the Non-Data descriptor
print(f"Status: {car.status}")

try:
    car.speed = 200 # Descriptor logic triggers ValueError
except ValueError as e:
    print(f"Error: {e}")

Descriptors & Properties

A Descriptor is the “boss” of an attribute. Instead of just storing a value, it defines methods for how that value is handled.

  • Data Descriptors (like ValidSpeed) define __set__. They are strict; even if you try to manually overwrite them in the object’s dictionary, the descriptor wins.

  • Non-Data Descriptors (like EngineStatus) only define __get__. They are “lazy” defaults—if you assign a value directly to the instance (e.g., car.status = “Broken”), that new value shadows the descriptor.

  • Properties are just a “shortcut” syntax for creating descriptors without writing a whole separate class.

Slots: The Speed Demon

Normally, every Python object has a __dict__ (a flexible but “heavy” dictionary) to store attributes.

  • __slots__ tells Python: “Don’t bother with a dictionary. I only need these specific variables.”

  • The tradeoff: It’s faster and uses way less RAM, but you can’t suddenly add a new attribute (like car.color = “Red”) if “color” wasn’t in the slots.

The “Catch-All” Hooks

__setattr__: This is your “Setter.” It’s great for logging or validation, but you must use super().__setattr__ inside it, or you’ll get stuck in an infinite loop (where the setter calls the setter… forever).

__getattribute__: This is the most powerful tool in the shed. It intercepts everything. Whether the attribute exists or not, this method gets called. Most people use __getattr__ (only called if the attribute isn’t found) instead, because __getattribute__ is so easy to break.