Classes, Instances, Attributes & Methods

Classes in Python are dynamic first-class objects created by the type metaclass at runtime. Understanding instance attribute dicts (__dict__), memory optimization via __slots__, bound vs unbound method descriptors (PyMethodObject), and @classmethod vs @staticmethod is fundamental for senior Python object-oriented design.

This chapter details CPython class/instance memory structures, __slots__ 60% memory reduction mechanics, method binding descriptors, and method dispatch types.


1. Class & Instance Memory Architecture (__dict__ vs. __slots__)

By default, every Python instance stores its dynamic attributes inside a heap-allocated dictionary (instance.__dict__).

Standard Instance Memory Layout (with __dict__):

[ PyObject Header (16B) ] -> [ __dict__ Pointer (8B) ] -> [ PyDictObject (~200B) ]
                                                            β”œβ”€β”€ "x": 10
                                                            └── "y": 20

While dynamic attribute assignment is flexible, allocating a 200-byte dictionary for millions of small objects creates massive RAM bloat.

Memory Optimization with __slots__:

Declaring __slots__ = ('x', 'y') inside a class tells CPython not to allocate an instance.__dict__. Instead, CPython allocates a fixed C array of attribute pointers directly inside the instance struct:

Optimized Instance Memory Layout (with __slots__):

[ PyObject Header (16B) ] -> [ Slot 0 ('x'): 10 (8B) ] -> [ Slot 1 ('y'): 20 (8B) ]
(Zero PyDictObject overhead! Saves 60%-70% RAM per instance!)

2. Method Binding Mechanics (PyMethodObject)

Functions defined inside a class are standard function objects (PyFunctionObject). Accessing a function through a class instance triggers its descriptor __get__() method, wrapping the function in a Bound Method (PyMethodObject):

class Vector:
    def __init__(self, x):
        self.x = x
    def magnitude(self):
        return self.x

v = Vector(10)

# Function accessed via Class -> Unbound Function
print(Vector.magnitude)  # <function Vector.magnitude at 0x...>

# Function accessed via Instance -> Bound Method Descriptor!
print(v.magnitude)       # <bound method Vector.magnitude of <Vector object at 0x...>>

A bound method automatically binds v as the first argument (self), transforming v.magnitude() into Vector.magnitude(v).


3. Method Types: Instance, @classmethod, and @staticmethod

class DatabasePool:
    def __init__(self, connection_str: str):
        self.connection_str = connection_str  # Instance Attribute

    # 1. Instance Method: Receives 'self' (instance pointer)
    def connect(self):
        return f"Connected to {self.connection_str}"

    # 2. Class Method: Receives 'cls' (class object) -> Alternative Factory Constructor
    @classmethod
    def from_env(cls):
        env_str = os.getenv("DATABASE_URL", "sqlite:///:memory:")
        return cls(env_str) # Instantiates class dynamically

    # 3. Static Method: Receives NO implicit first argument -> Pure Utility Function
    @staticmethod
    def validate_url(url: str) -> bool:
        return url.startswith("sqlite://") or url.startswith("postgresql://")

4. Production Trade-offs & __slots__ Inheritance

  • __slots__ Inheritance Invariant: Child classes must declare __slots__ = () explicitly. If a child class omits __slots__, CPython automatically allocates an instance.__dict__ for the child instance, negating the memory savings of the parent class!
  • __slots__ Limitations: Prevents assigning arbitrary un-slotted attributes at runtime and prevents using weakref unless '__weakref__' is explicitly included in __slots__.
Display Options
Appearance
Text Size
100%