__dict__ and __slots__ : two strategies for Python Attribute Storage

__dict__ vs __slots__: Deep Dive & Architectural Comparison

By default, Python objects store their instance attributes inside a dynamic hash map accessible via __dict__. While this provides maximum dynamism, it introduces significant memory overhead and indirection. The __slots__ declaration replaces this dictionary with fixed memory offsets at the C level.

Feature Default Behavior (__dict__) Optimized Layout (__slots__)
Storage Mechanism Dynamic PyDictObject per instance Fixed C-level array of pointer offsets (Descriptors)
Memory Overhead (per instance) High (~150–200 bytes base overhead) Minimal (~48–56 bytes base overhead)
Attribute Access Speed Standard hash map lookup ~15–25% faster via direct memory offset
Dynamic Access via getattr() / setattr() Supported for any arbitrary key name Supported strictly for keys declared in __slots__
Dynamic Attribute Assignment Arbitrary attributes allowed at runtime Restricted strictly to predefined names
Direct Dictionary Access (obj.__dict__) Supported (obj.__dict__['x']) ❌ Raises AttributeError (No __dict__)
Weak Reference Support Enabled by default Requires explicit '__weakref__' in slots
Introspection via vars() Supported directly ❌ Raises TypeError (no __dict__)

– Attribute assignment restrictions:
Declaring __slots__ creates an immutable attribute whitelist on the class layout:

class StandardPoint:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y
 
class SlottedPoint:
    __slots__ = ("x", "y")
 
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y
 
# 1. Standard instance allows dynamic attribute injection
p1 = StandardPoint(1.0, 2.0)
p1.z = 3.0  # Successfully added to p1.__dict__
 
# 2. Slotted instance prevents undeclared attribute assignment
p2 = SlottedPoint(1.0, 2.0)
try:
    p2.z = 3.0
except AttributeError as e:
    print(e)  # AttributeError: 'SlottedPoint' object has no attribute 'z'

Under the Hood: CPython Struct Offsets, Descriptors & Dynamic Access

– What declaring __slots__ = ("x", "y") actually produces:
1. CPython creates an internal C struct containing fixed pointer locations for x and y instead of allocating a PyDictObject.
2. Two member_descriptor objects are added to the class dictionary (SlottedVector.__dict__['x'] and SlottedVector.__dict__['y']).

– Dynamic attribute retrieval: getattr() vs __dict__:
You can still access slotted attributes dynamically at runtime using getattr() and setattr(), because Python routes these functions through the class descriptors. However, direct dictionary subscription is impossible since __dict__ does not exist.

class SlottedVector:
    __slots__ = ("x", "y")
 
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y
 
vec = SlottedVector(10.0, 20.0)
 
# 1. Inspecting class-level member descriptors
print(type(SlottedVector.x))  # <class 'member_descriptor'>
print(SlottedVector.x)        # <member 'x' of 'SlottedVector' objects>
 
# 2. Dynamic runtime access via getattr/setattr (Fully Functional):
attr_name = "x"
print(getattr(vec, attr_name))       # 10.0
setattr(vec, "y", 99.0)
print(vec.y)                         # 99.0
 
# 3. Dynamic assignment of UNDECLARED fields fails:
try:
    setattr(vec, "z", 30.0)
except AttributeError as e:
    print(e)  # AttributeError: 'SlottedVector' object has no attribute 'z'
 
# 4. Direct dict access fails (no internal dictionary):
try:
    print(vec.__dict__["x"])
except AttributeError as e:
    print(e)  # AttributeError: 'SlottedVector' object has no attribute '__dict__'

Slot Lifecycle: Uninitialized Attributes & Empty Slots ()

– 1. Uninitialized slots behavior (NULL pointer state):
Declaring a slot reserves memory space in C but does not assign a default value. Reading a declared slot before explicit assignment raises an AttributeError.

class PartialNode:
    __slots__ = ("id", "payload")
 
    def __init__(self, node_id: int) -> None:
        self.id = node_id
        # Note: 'payload' is declared in slots but not assigned here
 
node = PartialNode(1)
 
print(node.id)  # 1
 
# Accessing unassigned slot raises AttributeError:
try:
    print(node.payload)
except AttributeError as e:
    print(e)  # AttributeError: 'PartialNode' object has no attribute 'payload'
 
print(hasattr(node, "payload"))  # False (until assigned)
node.payload = {"data": 42}      # Assigning initializes the slot
print(node.payload)              # {'data': 42}

– 2. Completely empty slots: __slots__ = ():
If a class specifies an empty tuple __slots__ = (), it creates instances with zero instance-level attributes and no __dict__. This is widely used for stateless utilities and base mixin classes.

class ImmutableUtility:
    __slots__ = ()  # No attributes allowed, no __dict__ created
 
    def execute(self) -> str:
        return "Action performed"
 
util = ImmutableUtility()
 
# Any attribute assignment will be strictly rejected:
try:
    util.temp = "test"
except AttributeError as e:
    print(e)  # AttributeError: 'ImmutableUtility' object has no attribute 'temp'

Dynamic State & Metaprogramming: Working with __dict__ and vars()

– Metaprogramming and serialization workflows:
Standard classes expose their state via __dict__, making introspection, generic serialization, and runtime monkey-patching straightforward.

– Dynamic namespace inspection:

import types
 
class DynamicEntity:
    def __init__(self, name: str, role: str) -> None:
        self.name = name
        self.role = role
 
entity = DynamicEntity("Alice", "Admin")
 
# 1. Reading raw state mapping
print(entity.__dict__)  # {'name': 'Alice', 'role': 'Admin'}
print(vars(entity))      # Equivalent to entity.__dict__
 
# 2. Dynamic state mutation
entity.__dict__["status"] = "Active"
print(entity.status)     # "Active"
 
# 3. Dynamic execution into a module dictionary
dynamic_module = types.ModuleType("runtime_mod")
exec("def compute(x): return x * 2", dynamic_module.__dict__)
print(dynamic_module.compute(21))  # 42

Memory Optimization: Measuring Real-World Footprint

– Real memory allocation discrepancy:
sys.getsizeof() only measures the shallow memory of an instance pointer. For standard instances, it omits the separate memory block allocated for the inner __dict__ table.

import sys
 
class DefaultNode:
    def __init__(self, val: int) -> None:
        self.val = val
 
class SlottedNode:
    __slots__ = ("val",)
    def __init__(self, val: int) -> None:
        self.val = val
 
node_dict = DefaultNode(42)
node_slot = SlottedNode(42)
 
# Shallow size comparison:
print(sys.getsizeof(node_slot))  # ~48 bytes
print(sys.getsizeof(node_dict))  # ~48 bytes (shallow object header only)
 
# True size including the underlying dictionary:
dict_overhead = sys.getsizeof(node_dict.__dict__)
print(sys.getsizeof(node_dict) + dict_overhead)  # ~152 bytes (3x larger)

Modern Patterns: Dataclasses(slots=True), Weakrefs & Descriptors

– Python 3.10+ native slot generation:
Modern codebases avoid manual tuple definitions by leveraging the slots=True parameter in the standard dataclass decorator.

– Combining slots with optional features:

import weakref
from dataclasses import dataclass
 
# 1. Modern Dataclass with automated slots
@dataclass(slots=True)
class Coordinate:
    latitude: float
    longitude: float
 
# 2. Enabling Weak References alongside __slots__
class ObservableTask:
    __slots__ = ("task_id", "__weakref__")
 
    def __init__(self, task_id: str) -> None:
        self.task_id = task_id
 
task = ObservableTask("TASK-101")
ref = weakref.ref(task)
print(ref().task_id)  # "TASK-101"
 
# 3. Restoring dynamic attributes selectively by adding '__dict__' to slots
class HybridEntity:
    __slots__ = ("fixed_id", "__dict__")
 
    def __init__(self, fixed_id: int) -> None:
        self.fixed_id = fixed_id
 
hybrid = HybridEntity(1)
hybrid.arbitrary_tag = "custom"  # Works: dynamic fields stored in hybrid.__dict__

Inheritance Pitfalls, Layout Conflicts & Edge Cases

– 1. Implicit __dict__ reintroduction in subclasses:
If a parent class defines __slots__ but a derived subclass omits __slots__, the subclass automatically gains a __dict__, negating memory optimizations for derived instances.

class BaseSlot:
    __slots__ = ("a",)
 
class BrokenChild(BaseSlot):
    pass  # No __slots__ defined: __dict__ is created!
 
child = BrokenChild()
print(hasattr(child, "__dict__"))  # True

– 2. Multiple inheritance layout conflicts:
CPython prohibits inheriting from multiple parent classes that both define non-empty __slots__:

class PositionBase:
    __slots__ = ("x", "y")
 
class ColorBase:
    __slots__ = ("color",)
 
# Multiple inheritance with distinct non-empty slots fails:
try:
    class RenderedPoint(PositionBase, ColorBase):
        pass
except TypeError as e:
    print(e)  # TypeError: multiple bases have instance lay-out conflict

– 3. Safe mixin architecture:
To design mixins compatible with slotted classes, declare an empty slots tuple (__slots__ = ()) on abstract parents and mixin classes.

class LoggerMixin:
    __slots__ = ()  # Prevents __dict__ creation without causing struct layout conflicts
 
    def log(self, message: str) -> None:
        print(f"[LOG]: {message}")
 
class SecureEndpoint(BaseSlot, LoggerMixin):
    __slots__ = ("token",)
Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *