What is slots in Python?
In Python, `__slots__` is a special attribute that allows you to explicitly declare the data members (attributes) that an object of a class will have. By doing so, Python uses a more compact internal representation for instances, which can lead to significant memory savings and potentially faster attribute access, especially when creating a large number of objects.
What is __slots__?
By default, instances of user-defined classes in Python have a __dict__ attribute. This dictionary stores all instance-specific attributes, allowing for dynamic addition and removal of attributes at runtime. While flexible, this dictionary consumes memory, especially for objects that only need a few attributes.
The __slots__ attribute provides a way to tell Python not to create an instance __dict__ for objects of that class. Instead, it reserves a fixed amount of space for a predefined set of attributes. This fixed-size structure is more memory-efficient than a dictionary.
How __slots__ Works
When you define __slots__ in a class, you provide a tuple, list, or other iterable of strings, where each string is the name of an attribute the instances of that class will have. Python then uses these names to create descriptors for attribute access, rather than storing attributes in a __dict__.
class MyClassWithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
class MyClassWithoutSlots:
def __init__(self, x, y):
self.x = x
self.y = y
# Creating instances
obj_with_slots = MyClassWithSlots(10, 20)
obj_without_slots = MyClassWithoutSlots(10, 20)
# Accessing attributes
print(obj_with_slots.x) # Output: 10
print(obj_without_slots.y) # Output: 20
# Trying to add a new attribute to a slots object will fail (unless '__dict__' is in __slots__)
try:
obj_with_slots.z = 30
except AttributeError as e:
print(f"Error: {e}") # Output: Error: 'MyClassWithSlots' object has no attribute 'z'
# The object without slots can add new attributes dynamically
obj_without_slots.z = 30
print(obj_without_slots.z) # Output: 30
Benefits of using __slots__
- Memory Reduction: Significantly reduces the memory footprint of objects, especially when you have millions of instances. Each
__dict__can consume a substantial amount of memory. - Faster Attribute Access: Accessing attributes can sometimes be faster because Python doesn't need to look up attributes in a dictionary; instead, it accesses them directly at a fixed offset.
- Prevents Accidental Attribute Creation: By restricting the attributes an object can have,
__slots__helps prevent typos from inadvertently creating new attributes, which can mask bugs.
Drawbacks and Considerations
- Cannot Add New Attributes Dynamically: Unless
__dict__is explicitly included in__slots__, you cannot add new attributes to instances after they are created. - Inheritance Complexities: If a subclass does not define
__slots__, it will create a__dict__for its instances. If a subclass *does* define__slots__, its__slots__must be a superset of its parent's__slots__(or at least include its parent's slots). - Breaks
weakref: Instances cannot be targets ofweakrefs unless__weakref__is also explicitly listed in__slots__. - More Complex Class Design: Requires more explicit design decisions upfront about what attributes a class will have.
- No
__dict__means novars()on instances.
When to use __slots__
- Memory-Critical Applications: When dealing with a large number of objects (e.g., millions), where even a small memory saving per object adds up significantly.
- Performance Optimization: In scenarios where attribute access speed is paramount, though the performance gain might be less significant than memory savings.
- Immutable Data Structures: For classes designed to hold fixed data, where you want to enforce that no new attributes can be added.
Example: Memory Comparison
import sys
class PointWithoutSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class PointWithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p1 = PointWithoutSlots(1, 2)
p2 = PointWithSlots(1, 2)
print(f"Size of PointWithoutSlots instance: {sys.getsizeof(p1)} bytes")
print(f"Size of PointWithSlots instance: {sys.getsizeof(p2)} bytes")
# Check if __dict__ exists
print(f"PointWithoutSlots has __dict__: {'__dict__' in dir(p1)}")
print(f"PointWithSlots has __dict__: {'__dict__' in dir(p2)}")
Running the above code will typically show that PointWithSlots instances are significantly smaller than PointWithoutSlots instances because they lack the overhead of the __dict__.