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 ---------------- .. code-block:: python 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.