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.