__slots__
Dictionaries are the favorite workhouse of the Pythonic style.
Specifically in the cases of classes, instance attributes are stored in a __dict__ dunder, which is flexible, but uses a lot of overhead as each instance has
its own dictionary. Creating millions of class instances for small objects
can thus result in memory/performance issues.
Imagine, if you wanted to have a class that stored a datapoint as such:
class Point3D:
def __init__(self, x: float, y: float, z: float) -> None:
self.x = x
self.y = y
self.z = z
point1 = Point3D(1, 2, 3)
point1.__dict__ # This would be {\"x\": 1, \"y\": 2, \"z\": 3}
You may think it would be better to accomplish this with the @dataclass
decorator, but the underlying implementation of a @dataclass
is still a __dict__.
__slots__ is a memory-efficient alternative to
__dict__. When you define
__slots__, you declare what attributes an instance has,
resulting in a much more efficient storage mechanism. The underlying
implementation is a compact C array indexed by slot position.
class SlottedPoint3D:
__slots__ = ("x", "y", "z")
def __init__(self, x: float, y: float, z: float) -> None:
self.x = x
self.y = y
self.z = z
Python now allocates a fixed amount of memory with slot descriptor objects
with pre-defined methods like __get__ and
__set__ which index into the array. This direct memory lookup
is faster than a dictionary lookup. You can even use dataclasses with direct
slots support (Python 3.10+).
from dataclasses import dataclass
@dataclass(slots=True)
class SlottedPoint3D:
x: int
y: int
z: int
Here is a quick comparison after generating it. Check out the link here.
Memory per object (bytes)
| Type | dict | normal | slots |
|---|---|---|---|
| bytes | 424 | 584 | 152 |
Total creation time (seconds)
| N | dict | normal | slots |
|---|---|---|---|
| 10 | 0.000027 | 0.000011 | 0.000009 |
| 100 | 0.000044 | 0.000052 | 0.000041 |
| 1,000 | 0.000322 | 0.000501 | 0.000397 |
| 10,000 | 0.003644 | 0.004629 | 0.004458 |
| 100,000 | 0.047655 | 0.053937 | 0.047593 |
| 1,000,000 | 0.520809 | 0.531388 | 0.399850 |
Per-object creation time (nanoseconds)
| N | dict | normal | slots |
|---|---|---|---|
| 10 | 2672.10 | 1126.80 | 858.20 |
| 100 | 444.87 | 520.00 | 410.34 |
| 1,000 | 321.81 | 500.76 | 396.53 |
| 10,000 | 364.41 | 462.94 | 445.79 |
| 100,000 | 476.55 | 539.37 | 475.93 |
| 1,000,000 | 520.81 | 531.39 | 399.85 |
However, there is a tradeoff, here are some differences:
- You cannot add more attributes after predefining slots during instantiation.
- You cannot dynamically add attributes.
- You need to redeclare
__slots__in subclasses. - You cannot set class attributes for defaults in
__slots__(but you can add class-level constants).
Questions or feedback? Feel free to reach out!